Files
EonaCat.Sync/EonaCat.Sync/Merge/MergeStrategies.cs
T
2026-06-29 07:25:42 +02:00

318 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace EonaCat.Sync.Merge
{
/// <summary>
/// Interface for merge strategies.
/// </summary>
public interface IMergeStrategy
{
/// <summary>
/// Determines if this strategy can handle the file type.
/// </summary>
bool CanHandle(string filePath);
/// <summary>
/// Performs a 3-way merge operation.
/// </summary>
Task<MergeResult> MergeAsync(string baseFile, string sourceFile, string targetFile);
/// <summary>
/// Gets a human-readable name for the strategy.
/// </summary>
string Name { get; }
}
/// <summary>
/// Result of a merge operation.
/// </summary>
public class MergeResult
{
public bool Success { get; set; }
public string MergedContent { get; set; }
public List<MergeConflict> Conflicts { get; set; } = new List<MergeConflict>();
public int ConflictResolutionCount { get; set; }
}
/// <summary>
/// Represents a merge conflict.
/// </summary>
public class MergeConflict
{
public int LineNumber { get; set; }
public string BaseContent { get; set; }
public string SourceContent { get; set; }
public string TargetContent { get; set; }
public string Resolution { get; set; }
}
/// <summary>
/// Text file merge strategy with line-based 3-way merge.
/// </summary>
public class TextFileMergeStrategy : IMergeStrategy
{
public string Name { get { return "Text File Merge (Line-based)"; } }
public bool CanHandle(string filePath)
{
var extension = Path.GetExtension(filePath).ToLower();
return extension == ".txt" || extension == ".cs" || extension == ".java" ||
extension == ".cpp" || extension == ".py" || extension == ".js" ||
extension == ".config" || extension == ".properties";
}
public async Task<MergeResult> MergeAsync(string baseFile, string sourceFile, string targetFile)
{
var result = new MergeResult();
try
{
var baseLines = await ReadLinesAsync(baseFile);
var sourceLines = await ReadLinesAsync(sourceFile);
var targetLines = await ReadLinesAsync(targetFile);
var mergedLines = new List<string>();
int maxLines = Math.Max(Math.Max(baseLines.Count, sourceLines.Count), targetLines.Count);
for (int i = 0; i < maxLines; i++)
{
string baseLine = i < baseLines.Count ? baseLines[i] : string.Empty;
string sourceLine = i < sourceLines.Count ? sourceLines[i] : string.Empty;
string targetLine = i < targetLines.Count ? targetLines[i] : string.Empty;
if (baseLine == sourceLine && baseLine == targetLine)
{
mergedLines.Add(baseLine);
}
else if (baseLine == targetLine && sourceLine != baseLine)
{
mergedLines.Add(sourceLine);
}
else if (baseLine == sourceLine && targetLine != baseLine)
{
mergedLines.Add(targetLine);
}
else if (sourceLine == targetLine)
{
mergedLines.Add(sourceLine);
}
else
{
// Conflict
var resolution = sourceLine.Length >= targetLine.Length ? sourceLine : targetLine;
result.Conflicts.Add(new MergeConflict
{
LineNumber = i + 1,
BaseContent = baseLine,
SourceContent = sourceLine,
TargetContent = targetLine,
Resolution = resolution
});
mergedLines.Add(resolution);
result.ConflictResolutionCount++;
}
}
result.MergedContent = string.Join(Environment.NewLine, mergedLines);
result.Success = true;
}
catch (Exception ex)
{
result.Success = false;
result.MergedContent = $"Merge failed: {ex.Message}";
}
return result;
}
private async Task<List<string>> ReadLinesAsync(string filePath)
{
var lines = new List<string>();
if (!File.Exists(filePath))
return lines;
return await Task.Run(() =>
{
using (var reader = new StreamReader(filePath))
{
string line;
while ((line = reader.ReadLine()) != null)
{
lines.Add(line);
}
}
return lines;
});
}
}
/// <summary>
/// XML file merge strategy.
/// </summary>
public class XmlFileMergeStrategy : IMergeStrategy
{
public string Name { get { return "XML File Merge"; } }
public bool CanHandle(string filePath)
{
var ext = Path.GetExtension(filePath).ToLower();
return ext == ".xml" || ext == ".config";
}
public async Task<MergeResult> MergeAsync(string baseFile, string sourceFile, string targetFile)
{
var result = new MergeResult();
try
{
var sourceXml = await ReadXmlAsync(sourceFile);
var targetXml = await ReadXmlAsync(targetFile);
var mergedXml = MergeXmlElements(sourceXml, targetXml);
result.MergedContent = mergedXml.ToString();
result.Success = true;
}
catch (Exception ex)
{
result.Success = false;
result.MergedContent = $"XML merge failed: {ex.Message}";
}
return result;
}
private async Task<XElement> ReadXmlAsync(string filePath)
{
if (!File.Exists(filePath))
return new XElement("root");
return await Task.Run(() =>
{
var content = File.ReadAllText(filePath);
return XElement.Parse(content);
});
}
private XElement MergeXmlElements(XElement sourceElem, XElement targetElem)
{
var result = new XElement(sourceElem.Name);
// Merge attributes
foreach (var attr in sourceElem.Attributes())
{
result.SetAttributeValue(attr.Name, attr.Value);
}
foreach (var attr in targetElem.Attributes())
{
if (result.Attributes().FirstOrDefault(a => a.Name == attr.Name) == null)
{
result.SetAttributeValue(attr.Name, attr.Value);
}
}
// Merge elements
foreach (var elem in sourceElem.Elements())
{
result.Add(elem);
}
return result;
}
}
/// <summary>
/// Binary file merge strategy (simple).
/// </summary>
public class BinaryFileMergeStrategy : IMergeStrategy
{
public string Name { get { return "Binary File Merge"; } }
public bool CanHandle(string filePath)
{
return true; // Fallback for unknown types
}
public async Task<MergeResult> MergeAsync(string baseFile, string sourceFile, string targetFile)
{
var result = new MergeResult();
try
{
var sourceBytes = await ReadBytesAsync(sourceFile);
var targetBytes = await ReadBytesAsync(targetFile);
if (sourceBytes.SequenceEqual(targetBytes))
{
result.Success = true;
}
else
{
// Use larger file as resolution
result.MergedContent = "Binary conflict resolved using source file";
result.ConflictResolutionCount = 1;
result.Success = true;
}
}
catch (Exception ex)
{
result.Success = false;
result.MergedContent = $"Binary merge failed: {ex.Message}";
}
return result;
}
private async Task<byte[]> ReadBytesAsync(string filePath)
{
if (!File.Exists(filePath))
return new byte[0];
return await Task.Run(() => File.ReadAllBytes(filePath));
}
}
/// <summary>
/// Merge service that selects appropriate strategy for file type.
/// </summary>
public class MergeService
{
private readonly List<IMergeStrategy> _strategies;
public MergeService()
{
_strategies = new List<IMergeStrategy>
{
new XmlFileMergeStrategy(),
new TextFileMergeStrategy(),
new BinaryFileMergeStrategy()
};
}
/// <summary>
/// Merges three files using the appropriate strategy.
/// </summary>
public async Task<MergeResult> MergeFilesAsync(string baseFile, string sourceFile, string targetFile)
{
var strategy = _strategies.FirstOrDefault(s => s.CanHandle(sourceFile)) ??
_strategies.Last();
return await strategy.MergeAsync(baseFile, sourceFile, targetFile);
}
/// <summary>
/// Registers a custom merge strategy.
/// </summary>
public void RegisterStrategy(IMergeStrategy strategy)
{
_strategies.Insert(0, strategy);
}
}
}