sync-upstream
Version:
A tool for synchronizing code with upstream repositories with incremental updates and parallel processing.
415 lines (414 loc) • 20.1 kB
JavaScript
;
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;
}