// Real Git integration utilities for XFlow with Gitea // This handles actual Git operations, caching, and change tracking import { execSync } from 'child_process' import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs' import { join } from 'path' export interface GitRepository { id: string name: string owner: string url: string cloneUrl: string isPrivate: boolean type: 'personal' | 'group' ownerId: string localPath: string createdAt: string updatedAt: string } export interface GitCommit { id: string message: string author: string timestamp: string canvasId: string changes: { added: string[] modified: string[] deleted: string[] } } // Cache for Git repositories const gitCache = new Map() const canvasCache = new Map() // Base path for local Git repositories const GIT_BASE_PATH = process.env.GIT_BASE_PATH || '/tmp/xflow-git' // Ensure base directory exists if (!existsSync(GIT_BASE_PATH)) { mkdirSync(GIT_BASE_PATH, { recursive: true }) } // Create a new Git repository for a user export async function createPersonalRepository(userId: string, canvasId: string): Promise { const repoId = `repo-${userId}-${canvasId}-${Date.now()}` const repoName = `canvas-${canvasId}` const localPath = join(GIT_BASE_PATH, userId, repoName) // Create local directory if (!existsSync(localPath)) { mkdirSync(localPath, { recursive: true }) } // Initialize Git repository try { execSync('git init', { cwd: localPath }) execSync('git config user.name "XFlow System"', { cwd: localPath }) execSync('git config user.email "xflow@system.local"', { cwd: localPath }) } catch (error) { console.error('Failed to initialize Git repository:', error) } const repository: GitRepository = { id: repoId, name: repoName, owner: userId, url: `https://gitea.example.com/${userId}/${repoName}`, cloneUrl: `https://gitea.example.com/${userId}/${repoName}.git`, isPrivate: true, type: 'personal', ownerId: userId, localPath, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } gitCache.set(repoId, repository) // Initialize with initial commit await initializeRepository(repository, canvasId) return repository } // Initialize repository with initial canvas data async function initializeRepository(repository: GitRepository, canvasId: string): Promise { const canvasFile = join(repository.localPath, `${canvasId}.canvas`) // Create initial canvas file const initialCanvas = { nodes: [], edges: [] } writeFileSync(canvasFile, JSON.stringify(initialCanvas, null, 2)) // Create initial commit try { execSync('git add .', { cwd: repository.localPath }) execSync(`git commit -m "Initial commit for canvas ${canvasId}"`, { cwd: repository.localPath }) } catch (error) { console.error('Failed to create initial commit:', error) } } // Sync canvas changes to Git repository export async function syncCanvasToGit( canvasId: string, canvasData: any, commitMessage?: string ): Promise { // Find repository for this canvas const repository = Array.from(gitCache.values()) .find(repo => repo.name.includes(canvasId)) if (!repository) { throw new Error(`No Git repository found for canvas ${canvasId}`) } const canvasFile = join(repository.localPath, `${canvasId}.canvas`) // Check if file has changed const currentContent = JSON.stringify(canvasData, null, 2) let hasChanges = true if (existsSync(canvasFile)) { const existingContent = readFileSync(canvasFile, 'utf-8') hasChanges = existingContent !== currentContent } if (!hasChanges) { // Return latest commit if no changes const commits = getRepositoryHistory(repository) return commits[0] || { id: 'no-changes', message: 'No changes detected', author: 'system', timestamp: new Date().toISOString(), canvasId, changes: { added: [], modified: [], deleted: [] } } } // Write canvas data writeFileSync(canvasFile, currentContent) // Commit changes const message = commitMessage || `Update canvas ${canvasId}` try { execSync('git add .', { cwd: repository.localPath }) execSync(`git commit -m "${message}"`, { cwd: repository.localPath }) } catch (error) { console.error('Failed to commit changes:', error) } // Update cache repository.updatedAt = new Date().toISOString() canvasCache.set(canvasId, canvasData) // Get commit hash let commitHash = 'unknown' try { commitHash = execSync('git rev-parse HEAD', { cwd: repository.localPath, encoding: 'utf-8' }).trim() } catch (error) { console.error('Failed to get commit hash:', error) } const commit: GitCommit = { id: commitHash, message, author: repository.owner, timestamp: new Date().toISOString(), canvasId, changes: { added: existsSync(canvasFile) ? [`${canvasId}.canvas`] : [], modified: [`${canvasId}.canvas`], deleted: [] } } return commit } // Load canvas from Git repository export async function loadCanvasFromGit(canvasId: string): Promise { // Check cache first if (canvasCache.has(canvasId)) { return canvasCache.get(canvasId) } // Find repository for this canvas const repository = Array.from(gitCache.values()) .find(repo => repo.name.includes(canvasId)) if (!repository) { throw new Error(`No Git repository found for canvas ${canvasId}`) } const canvasFile = join(repository.localPath, `${canvasId}.canvas`) if (!existsSync(canvasFile)) { throw new Error(`Canvas file not found: ${canvasFile}`) } const canvasData = JSON.parse(readFileSync(canvasFile, 'utf-8')) // Cache the data canvasCache.set(canvasId, canvasData) return canvasData } // Get repository for a canvas export function getCanvasRepository(canvasId: string): GitRepository | null { return Array.from(gitCache.values()) .find(repo => repo.name.includes(canvasId)) || null } // Get commit history for a repository export function getRepositoryHistory(repository: GitRepository): GitCommit[] { try { const log = execSync('git log --pretty=format:"%H|%s|%an|%ad" --date=iso', { cwd: repository.localPath, encoding: 'utf-8' }) return log.split('\n') .filter(line => line.trim()) .map(line => { const [id, message, author, timestamp] = line.split('|') return { id, message, author, timestamp, canvasId: repository.name.replace('canvas-', ''), changes: { added: [], modified: [], deleted: [] } } }) } catch (error) { console.error('Failed to get repository history:', error) return [] } } // Check if canvas has unsaved changes export function hasUnsavedChanges(canvasId: string, currentData: any): boolean { if (!canvasCache.has(canvasId)) { return true // No cached data, assume changes } const cachedData = canvasCache.get(canvasId) return JSON.stringify(cachedData) !== JSON.stringify(currentData) } // Clear cache for a canvas export function clearCanvasCache(canvasId: string): void { canvasCache.delete(canvasId) }