Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions app/api/tasks/[taskId]/diff/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db/client'
import { tasks } from '@/lib/db/schema'
import { eq } from 'drizzle-orm'
import { octokit } from '@/lib/github/client'

function getLanguageFromFilename(filename: string): string {
const ext = filename.split('.').pop()?.toLowerCase()
const langMap: { [key: string]: string } = {
js: 'javascript',
jsx: 'javascript',
ts: 'typescript',
tsx: 'typescript',
py: 'python',
java: 'java',
cpp: 'cpp',
c: 'c',
cs: 'csharp',
php: 'php',
rb: 'ruby',
go: 'go',
rs: 'rust',
swift: 'swift',
kt: 'kotlin',
scala: 'scala',
sh: 'bash',
yaml: 'yaml',
yml: 'yaml',
json: 'json',
xml: 'xml',
html: 'html',
css: 'css',
scss: 'scss',
less: 'less',
md: 'markdown',
sql: 'sql',
}
return langMap[ext || ''] || 'text'
}

async function getFileContent(owner: string, repo: string, path: string, ref: string): Promise<string> {
try {
const response = await octokit.rest.repos.getContent({
owner,
repo,
path,
ref,
})

if ('content' in response.data && typeof response.data.content === 'string') {
return Buffer.from(response.data.content, 'base64').toString('utf-8')
}

return ''
} catch (error: unknown) {
// File might not exist in this ref (e.g., new file)
if (error && typeof error === 'object' && 'status' in error && error.status === 404) {
return ''
}
throw error
}
}

export async function GET(request: NextRequest, { params }: { params: Promise<{ taskId: string }> }) {
try {
const { taskId } = await params
const searchParams = request.nextUrl.searchParams
const filename = searchParams.get('filename')

if (!filename) {
return NextResponse.json({ error: 'Missing filename parameter' }, { status: 400 })
}

// Get task from database
const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId)).limit(1)

if (!task) {
return NextResponse.json({ error: 'Task not found' }, { status: 404 })
}

if (!task.branchName || !task.repoUrl) {
return NextResponse.json({ error: 'Task does not have branch or repository information' }, { status: 400 })
}

// Parse GitHub repository URL to get owner and repo
const githubMatch = task.repoUrl.match(/github\.com\/([^\/]+)\/([^\/\.]+)/)
if (!githubMatch) {
return NextResponse.json({ error: 'Invalid GitHub repository URL' }, { status: 400 })
}

const [, owner, repo] = githubMatch

try {
// Get file content from both main/master and the task branch
let oldContent = ''
let newContent = ''

// Try to get content from main branch first, fallback to master
try {
oldContent = await getFileContent(owner, repo, filename, 'main')
} catch (error: unknown) {
if (error && typeof error === 'object' && 'status' in error && error.status === 404) {
try {
oldContent = await getFileContent(owner, repo, filename, 'master')
} catch (masterError: unknown) {
if (
!(masterError && typeof masterError === 'object' && 'status' in masterError && masterError.status === 404)
) {
throw masterError
}
// File doesn't exist in main/master (could be a new file)
oldContent = ''
}
} else {
throw error
}
}

// Get content from the task branch
newContent = await getFileContent(owner, repo, filename, task.branchName)

return NextResponse.json({
success: true,
data: {
filename,
oldContent,
newContent,
language: getLanguageFromFilename(filename),
},
})
} catch (error: unknown) {
console.error('Error fetching file content from GitHub:', error)
return NextResponse.json({ error: 'Failed to fetch file content from GitHub' }, { status: 500 })
}
} catch (error) {
console.error('Error in diff API:', error)
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Internal server error',
},
{ status: 500 },
)
}
}
225 changes: 225 additions & 0 deletions app/api/tasks/[taskId]/files/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db/client'
import { tasks } from '@/lib/db/schema'
import { eq } from 'drizzle-orm'
import { octokit } from '@/lib/github/client'

interface FileChange {
filename: string
status: 'added' | 'modified' | 'deleted' | 'renamed'
additions: number
deletions: number
changes: number
}

interface FileTreeNode {
type: 'file' | 'directory'
filename?: string
status?: string
additions?: number
deletions?: number
changes?: number
children?: { [key: string]: FileTreeNode }
}

export async function GET(request: NextRequest, { params }: { params: Promise<{ taskId: string }> }) {
try {
const { taskId } = await params

// Get task from database
const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId)).limit(1)

if (!task) {
return NextResponse.json({ success: false, error: 'Task not found' }, { status: 404 })
}

// Check if task has a branch assigned
if (!task.branchName) {
return NextResponse.json({
success: true,
files: [],
fileTree: {},
branchName: null,
})
}

// Extract owner and repo from the repository URL
const repoUrl = task.repoUrl
if (!repoUrl) {
return NextResponse.json({
success: true,
files: [],
fileTree: {},
branchName: task.branchName,
})
}

// Parse GitHub repository URL to get owner and repo
const githubMatch = repoUrl.match(/github\.com\/([^\/]+)\/([^\/\.]+)/)
if (!githubMatch) {
console.error('Invalid GitHub URL format:', repoUrl)
return NextResponse.json(
{
success: false,
error: 'Invalid repository URL format',
},
{ status: 400 },
)
}

const [, owner, repo] = githubMatch

let files: FileChange[] = []

try {
// First check if the branch exists
try {
await octokit.rest.repos.getBranch({
owner,
repo,
branch: task.branchName,
})
} catch (branchError: unknown) {
if (branchError && typeof branchError === 'object' && 'status' in branchError && branchError.status === 404) {
// Branch doesn't exist yet (task is still processing)
console.log(`Branch ${task.branchName} doesn't exist yet, returning empty file list`)
return NextResponse.json({
success: true,
files: [],
fileTree: {},
branchName: task.branchName,
message: 'Branch is being created...',
})
} else {
throw branchError
}
}

// Try to get the comparison between the branch and main
let comparison
try {
comparison = await octokit.rest.repos.compareCommits({
owner,
repo,
base: 'main',
head: task.branchName,
})
} catch (mainError: unknown) {
if (mainError && typeof mainError === 'object' && 'status' in mainError && mainError.status === 404) {
// If main branch doesn't exist, try master
try {
comparison = await octokit.rest.repos.compareCommits({
owner,
repo,
base: 'master',
head: task.branchName,
})
} catch (masterError: unknown) {
if (
masterError &&
typeof masterError === 'object' &&
'status' in masterError &&
masterError.status === 404
) {
// Neither main nor master exists, or head branch doesn't exist
console.log(`Could not compare branches for ${task.branchName}`)
return NextResponse.json({
success: true,
files: [],
fileTree: {},
branchName: task.branchName,
message: 'No base branch found for comparison',
})
} else {
throw masterError
}
}
} else {
throw mainError
}
}

// Convert GitHub API response to our FileChange format
files =
comparison.data.files?.map((file) => ({
filename: file.filename,
status: file.status as 'added' | 'modified' | 'deleted' | 'renamed',
additions: file.additions || 0,
deletions: file.deletions || 0,
changes: file.changes || 0,
})) || []

console.log(`Found ${files.length} changed files in branch ${task.branchName}`)
} catch (error: unknown) {
console.error('Error fetching file changes from GitHub:', error)

// If it's a 404 error, return empty results instead of failing
if (error && typeof error === 'object' && 'status' in error && error.status === 404) {
console.log(`Branch or repository not found, returning empty file list`)
return NextResponse.json({
success: true,
files: [],
fileTree: {},
branchName: task.branchName,
message: 'Branch not found or still being created',
})
}

return NextResponse.json(
{
success: false,
error: 'Failed to fetch file changes from GitHub',
},
{ status: 500 },
)
}

// Build file tree from files
const fileTree: { [key: string]: FileTreeNode } = {}

for (const file of files) {
addToFileTree(fileTree, file.filename, file)
}

return NextResponse.json({
success: true,
files,
fileTree,
branchName: task.branchName,
})
} catch (error) {
console.error('Error fetching task files:', error)
return NextResponse.json({ success: false, error: 'Failed to fetch task files' }, { status: 500 })
}
}

function addToFileTree(tree: { [key: string]: FileTreeNode }, filename: string, fileObj: FileChange) {
const parts = filename.split('/')
let currentLevel = tree

for (let i = 0; i < parts.length; i++) {
const part = parts[i]
const isLastPart = i === parts.length - 1

if (isLastPart) {
// It's a file
currentLevel[part] = {
type: 'file',
filename: fileObj.filename,
status: fileObj.status,
additions: fileObj.additions,
deletions: fileObj.deletions,
changes: fileObj.changes,
}
} else {
// It's a directory
if (!currentLevel[part]) {
currentLevel[part] = {
type: 'directory',
children: {},
}
}
currentLevel = currentLevel[part].children!
}
}
}
Loading
Loading