UNPKG

sync-upstream

Version:

A tool for synchronizing code with upstream repositories with incremental updates and parallel processing.

415 lines (414 loc) 20.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConflictResolver = exports.ConflictResolutionStrategy = exports.ConflictType = void 0; const node_path_1 = __importDefault(require("node:path")); const chalk_1 = __importDefault(require("chalk")); const fs_extra_1 = __importDefault(require("fs-extra")); const prompts_1 = __importDefault(require("prompts")); const errors_1 = require("./errors"); const hash_1 = require("./hash"); const logger_1 = require("./logger"); /** * 冲突类型枚举 */ var ConflictType; (function (ConflictType) { /** 文件内容冲突 */ ConflictType["CONTENT"] = "content"; /** 文件类型冲突(一个是文件,一个是目录) */ ConflictType["TYPE"] = "type"; /** 重命名冲突 */ ConflictType["RENAME"] = "rename"; })(ConflictType || (exports.ConflictType = ConflictType = {})); /** * 冲突解决策略枚举 */ var ConflictResolutionStrategy; (function (ConflictResolutionStrategy) { /** 使用源文件覆盖目标文件 */ ConflictResolutionStrategy["USE_SOURCE"] = "use-source"; /** 保留目标文件 */ ConflictResolutionStrategy["KEEP_TARGET"] = "keep-target"; /** 尝试自动合并(仅适用于文本文件) */ ConflictResolutionStrategy["AUTO_MERGE"] = "auto-merge"; /** 提示用户解决 */ ConflictResolutionStrategy["PROMPT_USER"] = "prompt-user"; })(ConflictResolutionStrategy || (exports.ConflictResolutionStrategy = ConflictResolutionStrategy = {})); /** * 冲突解决器类 */ class ConflictResolver { /** * 构造函数 * @param config 冲突解决配置 */ constructor(config) { this.config = config; } /** * 检测文件冲突 * @param sourcePath 源文件路径 * @param targetPath 目标文件路径 * @returns 冲突信息,如果没有冲突则返回null */ async detectFileConflict(sourcePath, targetPath) { try { const sourceExists = await fs_extra_1.default.pathExists(sourcePath); const targetExists = await fs_extra_1.default.pathExists(targetPath); // 只有当源文件和目标文件都存在时才可能有冲突 if (!sourceExists || !targetExists) { return null; } // 检查是否一个是文件,一个是目录 const sourceStat = await fs_extra_1.default.stat(sourcePath); const targetStat = await fs_extra_1.default.stat(targetPath); if (sourceStat.isDirectory() !== targetStat.isDirectory()) { return { type: ConflictType.TYPE, sourcePath, targetPath, sourceType: sourceStat.isDirectory() ? 'directory' : 'file', targetType: targetStat.isDirectory() ? 'directory' : 'file', }; } // 如果都是目录,没有冲突 if (sourceStat.isDirectory() && targetStat.isDirectory()) { return null; } // 如果都是文件,比较内容哈希 const sourceHash = await (0, hash_1.getFileHash)(sourcePath); const targetHash = await (0, hash_1.getFileHash)(targetPath); if (sourceHash !== targetHash) { return { type: ConflictType.CONTENT, sourcePath, targetPath, sourceHash, targetHash, }; } // 没有冲突 return null; } catch (error) { throw new errors_1.FsError(`检测文件冲突时出错: ${sourcePath} vs ${targetPath}`, error); } } /** * 检测目录冲突 * @param sourceDir 源目录路径 * @param targetDir 目标目录路径 * @param ignorePatterns 忽略模式 * @returns 冲突信息列表 */ async detectDirectoryConflicts(sourceDir, targetDir, ignorePatterns = []) { const conflicts = []; try { // 读取源目录 const sourceEntries = await fs_extra_1.default.readdir(sourceDir, { withFileTypes: true }); for (const entry of sourceEntries) { const sourcePath = node_path_1.default.join(sourceDir, entry.name); const targetPath = node_path_1.default.join(targetDir, entry.name); const relativePath = node_path_1.default.relative(process.cwd(), sourcePath); // 检查是否应该忽略 if (shouldIgnore(relativePath, ignorePatterns)) { continue; } if (entry.isDirectory()) { // 递归检查子目录 const subConflicts = await this.detectDirectoryConflicts(sourcePath, targetPath, ignorePatterns); conflicts.push(...subConflicts); } else { // 检查文件冲突 const conflict = await this.detectFileConflict(sourcePath, targetPath); if (conflict) { conflicts.push(conflict); } } } return conflicts; } catch (error) { throw new errors_1.FsError(`检测目录冲突时出错: ${sourceDir} vs ${targetDir}`, error); } } /** * 解决单个冲突 * @param conflict 冲突信息 * @param strategy 解决策略(可选,默认使用配置中的策略) * @returns 是否成功解决 */ async resolveConflict(conflict, strategy) { let resolutionStrategy = strategy || this.config.defaultStrategy; // 检查是否应该自动解决 if (conflict.type === ConflictType.CONTENT && this.config.autoResolveTypes) { const fileExtension = node_path_1.default.extname(conflict.sourcePath).toLowerCase(); if (this.config.autoResolveTypes.includes(fileExtension)) { logger_1.logger.debug(`自动解决冲突: ${conflict.sourcePath} (匹配自动解决类型 ${fileExtension})`); // 如果是自动解决类型,使用配置中的默认策略 if (!strategy) { resolutionStrategy = this.config.defaultStrategy; } } } try { switch (conflict.type) { case ConflictType.CONTENT: return this.resolveContentConflict(conflict, resolutionStrategy); case ConflictType.TYPE: return this.resolveTypeConflict(conflict, resolutionStrategy); case ConflictType.RENAME: return this.resolveRenameConflict(conflict, resolutionStrategy); default: logger_1.logger.error(`未知的冲突类型: ${conflict.type}`); return false; } } catch (error) { logger_1.logger.error(`解决冲突时出错: ${error instanceof Error ? error.message : String(error)}`); return false; } } /** * 解决内容冲突 * @param conflict 冲突信息 * @param strategy 解决策略 * @returns 是否成功解决 */ async resolveContentConflict(conflict, strategy) { // 实现内容冲突解决逻辑 let resolved = false; const fileExtension = node_path_1.default.extname(conflict.sourcePath).toLowerCase(); const isAutoResolveType = this.config.autoResolveTypes?.includes(fileExtension) || false; switch (strategy) { case ConflictResolutionStrategy.USE_SOURCE: await fs_extra_1.default.copyFile(conflict.sourcePath, conflict.targetPath); logger_1.logger.info(`冲突解决: 使用源文件覆盖目标文件 ${chalk_1.default.yellow(conflict.targetPath)}`); resolved = true; break; case ConflictResolutionStrategy.KEEP_TARGET: logger_1.logger.info(`冲突解决: 保留目标文件 ${chalk_1.default.yellow(conflict.targetPath)}`); resolved = true; break; case ConflictResolutionStrategy.AUTO_MERGE: // 尝试自动合并(这里简化实现,实际项目中可能需要更复杂的合并逻辑) try { const sourceContent = await fs_extra_1.default.readFile(conflict.sourcePath, 'utf8'); const targetContent = await fs_extra_1.default.readFile(conflict.targetPath, 'utf8'); // 简单的合并策略:保留双方内容并添加标记 const mergedContent = `<<<<<<< SOURCE ${sourceContent} ======= ${targetContent} >>>>>>> TARGET`; await fs_extra_1.default.writeFile(conflict.targetPath, mergedContent); logger_1.logger.info(`冲突解决: 自动合并文件 ${chalk_1.default.yellow(conflict.targetPath)}`); resolved = true; } catch (error) { logger_1.logger.error(`自动合并失败,回退到提示用户: ${error instanceof Error ? error.message : String(error)}`); return this.resolveContentConflict(conflict, ConflictResolutionStrategy.PROMPT_USER); } break; case ConflictResolutionStrategy.PROMPT_USER: // 如果是自动解决类型,则不提示用户,直接使用默认策略 if (isAutoResolveType) { logger_1.logger.debug(`文件 ${chalk_1.default.yellow(conflict.targetPath)} 是自动解决类型,使用默认策略 ${this.config.defaultStrategy}`); return this.resolveContentConflict(conflict, this.config.defaultStrategy); } // 提示用户解决 const { resolution } = await (0, prompts_1.default)({ type: 'select', name: 'resolution', message: `文件 ${chalk_1.default.yellow(conflict.targetPath)} 存在内容冲突,如何解决?`, choices: [ { title: '使用源文件覆盖', value: ConflictResolutionStrategy.USE_SOURCE }, { title: '保留目标文件', value: ConflictResolutionStrategy.KEEP_TARGET }, { title: '查看并编辑合并结果', value: 'edit' }, ], }); if (resolution === 'edit') { // 在实际项目中,这里可以打开编辑器让用户手动合并 logger_1.logger.info(`提示: 请手动编辑文件 ${chalk_1.default.yellow(conflict.targetPath)} 解决冲突`); return false; } else { return this.resolveContentConflict(conflict, resolution); } default: logger_1.logger.error(`未知的解决策略: ${strategy}`); return false; } // 记录冲突解决日志 if (resolved && this.config.logResolutions) { logger_1.logger.debug(`冲突解决日志: 类型=${conflict.type}, 源文件=${conflict.sourcePath}, 目标文件=${conflict.targetPath}, 策略=${strategy}`); } return resolved; } /** * 解决类型冲突 * @param conflict 冲突信息 * @param strategy 解决策略 * @returns 是否成功解决 */ async resolveTypeConflict(conflict, strategy) { // 实现类型冲突解决逻辑 let resolved = false; // 对于类型冲突,我们可以考虑文件名的扩展名来决定是否自动解决 const fileExtension = node_path_1.default.extname(conflict.sourcePath).toLowerCase(); const isAutoResolveType = this.config.autoResolveTypes?.includes(fileExtension) || false; switch (strategy) { case ConflictResolutionStrategy.USE_SOURCE: // 删除目标,复制源 if (await fs_extra_1.default.pathExists(conflict.targetPath)) { await fs_extra_1.default.remove(conflict.targetPath); } if (conflict.sourceType === 'directory') { await fs_extra_1.default.mkdir(conflict.targetPath, { recursive: true }); // 递归复制目录内容 await fs_extra_1.default.copy(conflict.sourcePath, conflict.targetPath); } else { await fs_extra_1.default.copyFile(conflict.sourcePath, conflict.targetPath); } logger_1.logger.info(`冲突解决: 使用源${conflict.sourceType}覆盖目标${conflict.targetType} ${chalk_1.default.yellow(conflict.targetPath)}`); resolved = true; break; case ConflictResolutionStrategy.KEEP_TARGET: logger_1.logger.info(`冲突解决: 保留目标${conflict.targetType} ${chalk_1.default.yellow(conflict.targetPath)}`); resolved = true; break; case ConflictResolutionStrategy.PROMPT_USER: // 如果是自动解决类型,则不提示用户,直接使用默认策略 if (isAutoResolveType) { logger_1.logger.debug(`路径 ${chalk_1.default.yellow(conflict.targetPath)} 是自动解决类型,使用默认策略 ${this.config.defaultStrategy}`); return this.resolveTypeConflict(conflict, this.config.defaultStrategy); } // 提示用户解决 const { resolution } = await (0, prompts_1.default)({ type: 'select', name: 'resolution', message: `路径 ${chalk_1.default.yellow(conflict.targetPath)} 存在类型冲突(源是${conflict.sourceType},目标是${conflict.targetType}),如何解决?`, choices: [ { title: `使用源${conflict.sourceType}覆盖`, value: ConflictResolutionStrategy.USE_SOURCE }, { title: `保留目标${conflict.targetType}`, value: ConflictResolutionStrategy.KEEP_TARGET }, ], }); return this.resolveTypeConflict(conflict, resolution); default: logger_1.logger.error(`未知的解决策略: ${strategy}`); return false; } // 记录冲突解决日志 if (resolved && this.config.logResolutions) { logger_1.logger.debug(`冲突解决日志: 类型=${conflict.type}, 源文件=${conflict.sourcePath}, 目标文件=${conflict.targetPath}, 策略=${strategy}`); } return resolved; } /** * 解决重命名冲突 * @param conflict 冲突信息 * @param strategy 解决策略 * @returns 是否成功解决 */ async resolveRenameConflict(conflict, strategy) { let resolved = false; // 对于重命名冲突,我们可以考虑文件名的扩展名来决定是否自动解决 const fileExtension = node_path_1.default.extname(conflict.sourcePath).toLowerCase(); const isAutoResolveType = this.config.autoResolveTypes?.includes(fileExtension) || false; switch (strategy) { case ConflictResolutionStrategy.USE_SOURCE: // 确保目标路径不存在 if (await fs_extra_1.default.pathExists(conflict.targetPath)) { await fs_extra_1.default.remove(conflict.targetPath); } // 复制源文件到目标路径 if (await fs_extra_1.default.pathExists(conflict.sourcePath)) { const sourceStat = await fs_extra_1.default.stat(conflict.sourcePath); if (sourceStat.isDirectory()) { await fs_extra_1.default.mkdir(conflict.targetPath, { recursive: true }); await fs_extra_1.default.copy(conflict.sourcePath, conflict.targetPath); } else { await fs_extra_1.default.copyFile(conflict.sourcePath, conflict.targetPath); } logger_1.logger.info(`冲突解决: 使用源文件 ${chalk_1.default.yellow(conflict.sourcePath)} 覆盖目标路径 ${chalk_1.default.yellow(conflict.targetPath)}`); resolved = true; } else { logger_1.logger.error(`源文件不存在: ${conflict.sourcePath}`); } break; case ConflictResolutionStrategy.KEEP_TARGET: logger_1.logger.info(`冲突解决: 保留目标文件 ${chalk_1.default.yellow(conflict.targetPath)}`); resolved = true; break; case ConflictResolutionStrategy.PROMPT_USER: // 如果是自动解决类型,则不提示用户,直接使用默认策略 if (isAutoResolveType) { logger_1.logger.debug(`文件 ${chalk_1.default.yellow(conflict.sourcePath)} 是自动解决类型,使用默认策略 ${this.config.defaultStrategy}`); return this.resolveRenameConflict(conflict, this.config.defaultStrategy); } // 提示用户解决 const { resolution } = await (0, prompts_1.default)({ type: 'select', name: 'resolution', message: `检测到重命名冲突: ${chalk_1.default.yellow(conflict.sourcePath)} -> ${chalk_1.default.yellow(conflict.targetPath)},如何解决?`, choices: [ { title: '使用源文件覆盖目标路径', value: ConflictResolutionStrategy.USE_SOURCE }, { title: '保留目标文件', value: ConflictResolutionStrategy.KEEP_TARGET }, ], }); return this.resolveRenameConflict(conflict, resolution); default: logger_1.logger.error(`未知的解决策略: ${strategy}`); return false; } // 记录冲突解决日志 if (resolved && this.config.logResolutions) { logger_1.logger.debug(`冲突解决日志: 类型=${conflict.type}, 源文件=${conflict.sourcePath}, 目标文件=${conflict.targetPath}, 策略=${strategy}`); } return resolved; } /** * 解决多个冲突 * @param conflicts 冲突信息列表 * @returns 成功解决的冲突数量 */ async resolveConflicts(conflicts) { let resolvedCount = 0; if (conflicts.length === 0) { logger_1.logger.info('没有检测到冲突'); return resolvedCount; } logger_1.logger.warn(chalk_1.default.yellow(`检测到 ${conflicts.length} 个冲突`)); for (const conflict of conflicts) { const resolved = await this.resolveConflict(conflict); if (resolved) { resolvedCount++; } } logger_1.logger.info(`成功解决 ${resolvedCount}/${conflicts.length} 个冲突`); return resolvedCount; } } exports.ConflictResolver = ConflictResolver; /** * 检查路径是否应该被忽略 * @param path 路径 * @param ignorePatterns 忽略模式列表 * @returns 是否应该被忽略 */ function shouldIgnore(path, ignorePatterns) { // 简化实现,实际项目中可能需要使用更复杂的模式匹配 for (const pattern of ignorePatterns) { if (path.includes(pattern)) { return true; } } return false; }