UNPKG

@iyulab/oops

Version:

Core SDK for Oops - Safe text file editing with automatic backup

321 lines 11.7 kB
"use strict"; /** * VersionManager - Git-style version management system * Implements semantic versioning with branching (1.0 → 1.1 → 1.1.1) */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.VersionManager = void 0; const git_wrapper_1 = require("./git-wrapper"); const file_system_1 = require("./file-system"); const errors_1 = require("./errors"); const path = __importStar(require("path")); const crypto = __importStar(require("crypto")); class VersionManager { git; repoPath; filePath; constructor(repoPath, filePath) { this.repoPath = repoPath; this.filePath = filePath; this.git = new git_wrapper_1.GitWrapper(repoPath); } /** * Initialize version system for a file */ async initialize() { // Ensure repository directory exists await file_system_1.FileSystem.ensureDir(this.repoPath); // Initialize Git repository if (!(await this.git.isHealthy())) { await this.git.init(); } // Create initial version if file doesn't exist in repo const fileName = path.basename(this.filePath); const repoFilePath = path.join(this.repoPath, fileName); if (!(await file_system_1.FileSystem.exists(repoFilePath))) { // Copy file to repo and create version 1.0 await file_system_1.FileSystem.copy(this.filePath, repoFilePath); await this.git.add(fileName); await this.git.commit('Initial version 1.0'); await this.createTag('1.0', 'Initial version'); } } /** * Create a new version from current file state */ async commit(message) { const fileName = path.basename(this.filePath); const repoFilePath = path.join(this.repoPath, fileName); // Copy current file to repo await file_system_1.FileSystem.copy(this.filePath, repoFilePath); // Check if there are any changes const status = await this.git.getStatus(); const hasChanges = status.files.length > 0; if (!hasChanges) { return null; // No changes to commit } // Determine next version number const currentVersion = await this.getCurrentVersion(); const nextVersion = await this.getNextVersion(currentVersion); // Create commit await this.git.add(fileName); const commitMessage = message || `Version ${nextVersion}`; await this.git.commit(commitMessage); // Create version tag await this.createTag(nextVersion, commitMessage); return { filePath: this.filePath, version: nextVersion, message: commitMessage, previousVersion: currentVersion, }; } /** * Checkout a specific version */ async checkout(version) { const fileName = path.basename(this.filePath); const repoFilePath = path.join(this.repoPath, fileName); // Validate version exists const versions = await this.getAllVersions(); if (!versions.find(v => v.version === version)) { throw new errors_1.OopsError(`Version ${version} not found`); } // Checkout the version tag await this.git.checkout(`refs/tags/${version}`); // Copy file back to original location if (await file_system_1.FileSystem.exists(repoFilePath)) { await file_system_1.FileSystem.copy(repoFilePath, this.filePath); } } /** * Get diff between versions */ async getDiff(fromVersion, toVersion) { const fileName = path.basename(this.filePath); if (!fromVersion && !toVersion) { // Diff against working directory return await this.git.diff(fileName); } if (fromVersion && !toVersion) { // Diff current working directory against specific version return await this.git.diff(fileName, `refs/tags/${fromVersion}`); } if (fromVersion && toVersion) { // Diff between two versions return await this.git.diff(fileName, `refs/tags/${fromVersion}`, `refs/tags/${toVersion}`); } return ''; } /** * Check if file has changes since last version */ async hasChanges() { const fileName = path.basename(this.filePath); const repoFilePath = path.join(this.repoPath, fileName); // Copy current file to repo temporarily to check diff const tempPath = repoFilePath + '.temp'; await file_system_1.FileSystem.copy(this.filePath, tempPath); try { // Compare with current repo version const currentContent = await file_system_1.FileSystem.readFile(repoFilePath); const newContent = await file_system_1.FileSystem.readFile(tempPath); return currentContent !== newContent; } finally { // Clean up temp file if (await file_system_1.FileSystem.exists(tempPath)) { await file_system_1.FileSystem.remove(tempPath); } } } /** * Get all versions in chronological order */ async getAllVersions() { const tags = await this.git.getTags(); const versions = []; for (const tag of tags) { try { const commitInfo = await this.git.getCommitInfo(tag); const checksum = await this.getVersionChecksum(tag); versions.push({ version: tag, message: commitInfo.message, timestamp: commitInfo.date, checksum, parentVersions: await this.getParentVersions(tag), }); } catch { // Skip invalid tags } } // Sort by semantic version return versions.sort((a, b) => this.compareVersions(a.version, b.version)); } /** * Get current version (latest tag) */ async getCurrentVersion() { const versions = await this.getAllVersions(); return versions.length > 0 ? versions[versions.length - 1].version : null; } /** * Get version history as a tree structure */ async getVersionTree() { const versions = await this.getAllVersions(); const tree = []; // Build tree structure based on version numbers for (const version of versions) { const node = { version: version.version, message: version.message, timestamp: version.timestamp, checksum: version.checksum, children: [], }; // Determine where to place this version in the tree this.insertVersionInTree(tree, node); } return tree; } /** * Remove all version data */ async cleanup() { if (await file_system_1.FileSystem.exists(this.repoPath)) { await file_system_1.FileSystem.remove(this.repoPath); } } // Private helper methods async createTag(version, message) { await this.git.tag(version, message); } async getNextVersion(currentVersion) { if (!currentVersion) { return '1.0'; } // Parse current version const parts = currentVersion.split('.').map(Number); // Determine if we're creating a branch or continuing sequence const currentBranch = await this.git.getCurrentBranch(); const isDetached = currentBranch === 'HEAD'; // Detached head means we're on a tag if (isDetached) { // We're on a specific version tag, create a branch if (parts.length === 2) { // 1.2 → 1.2.1 return `${parts[0]}.${parts[1]}.1`; } else { // 1.2.1 → 1.2.1.1 return currentVersion + '.1'; } } else { // We're on main branch, increment last number if (parts.length === 2) { // 1.1 → 1.2 return `${parts[0]}.${parts[1] + 1}`; } else { // 1.2.1 → 1.2.2 parts[parts.length - 1]++; return parts.join('.'); } } } async getVersionChecksum(version) { const fileName = path.basename(this.filePath); try { const content = await this.git.getFileAtVersion(fileName, `refs/tags/${version}`); return crypto.createHash('sha256').update(content).digest('hex').slice(0, 8); } catch { return 'unknown'; } } async getParentVersions(version) { // For now, return empty array - could implement Git parent tracking return []; } compareVersions(a, b) { const aParts = a.split('.').map(Number); const bParts = b.split('.').map(Number); for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { const aVal = aParts[i] || 0; const bVal = bParts[i] || 0; if (aVal !== bVal) { return aVal - bVal; } } return 0; } insertVersionInTree(tree, node) { const versionParts = node.version.split('.').map(Number); if (versionParts.length === 2) { // Main branch version (e.g., 1.0, 1.1) tree.push(node); } else { // Branch version (e.g., 1.1.1, 1.1.1.1) const parentVersion = versionParts.slice(0, -1).join('.'); const parent = this.findVersionInTree(tree, parentVersion); if (parent) { parent.children.push(node); } else { // Parent not found, add to root tree.push(node); } } } findVersionInTree(tree, version) { for (const node of tree) { if (node.version === version) { return node; } const found = this.findVersionInTree(node.children, version); if (found) { return found; } } return null; } } exports.VersionManager = VersionManager; //# sourceMappingURL=version-manager.js.map