265 lines
7.2 KiB
TypeScript
265 lines
7.2 KiB
TypeScript
// 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<string, GitRepository>()
|
|
const canvasCache = new Map<string, any>()
|
|
|
|
// 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<GitRepository> {
|
|
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<void> {
|
|
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<GitCommit> {
|
|
// 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<any> {
|
|
// 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)
|
|
}
|