@iyulab/oops
Version:
Core SDK for Oops - Safe text file editing with automatic backup
321 lines • 11.7 kB
JavaScript
"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