DiffConflicts

This commit is contained in:
timerix 2023-03-18 05:08:12 +06:00
parent d9f14d3c11
commit 1299e83d4e
8 changed files with 354 additions and 123 deletions

View File

@ -4,60 +4,70 @@ using System.Text;
using DiffMatchPatch; using DiffMatchPatch;
using DTLib.Filesystem; using DTLib.Filesystem;
Console.InputEncoding=Encoding.UTF8; namespace diff_text;
Console.OutputEncoding=Encoding.UTF8;
if (args.Length != 2) public static class DiffText
{ {
internal static void Main(string[] args)
{
Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;
if (args.Length != 2)
{
Console.WriteLine("usage: [file0] [file1]"); Console.WriteLine("usage: [file0] [file1]");
return; return;
} }
var _diff=FileDiff(args[0], args[1]); var _diff = FileDiff(args[0], args[1]);
PrintDiff(_diff); PrintDiff(_diff);
}
public static List<Diff> FileDiff(string file0, string file1)
List<Diff> FileDiff(string file0, string file1) {
{
string fileText0 = File.ReadAllText(file0); string fileText0 = File.ReadAllText(file0);
string fileText1 = File.ReadAllText(file1); string fileText1 = File.ReadAllText(file1);
return TextDiff(fileText0, fileText1); return TextDiff(fileText0, fileText1);
} }
List<Diff> TextDiff(string text0, string text1) public static List<Diff> TextDiff(string text0, string text1)
{ {
var diff = Diff.Compute(text0, text1, checklines:true); var diff = Diff.Compute(text0, text1, checklines: true);
diff.CleanupSemantic(); diff.CleanupSemantic();
return diff; return diff;
} }
void PrintDiff(List<Diff> diff, bool ignoreWhitespaces=false) public static void PrintDiff(List<Diff> diff, bool ignoreWhitespaces = false)
{ {
Console.ResetColor();
foreach (var d in diff) foreach (var d in diff)
{ {
bool whitespaceOnly = d.WhitespaceOnlyDiff; bool whitespaceOnly = d.WhitespaceOnlyDiff;
if(ignoreWhitespaces && whitespaceOnly) if (ignoreWhitespaces && whitespaceOnly)
continue; continue;
switch(d.Operation) string text;
switch (d.Operation)
{ {
case Operation.Delete: case Operation.Delete:
Console.BackgroundColor = ConsoleColor.DarkRed; Console.BackgroundColor = ConsoleColor.DarkRed;
Console.ForegroundColor = ConsoleColor.Black; Console.ForegroundColor = ConsoleColor.Black;
Console.Write(whitespaceOnly ? d.FormattedText : d.Text); text = whitespaceOnly ? d.FormattedText : d.Text;
Console.ResetColor();
break; break;
case Operation.Insert: case Operation.Insert:
Console.BackgroundColor = ConsoleColor.DarkGreen; Console.BackgroundColor = ConsoleColor.DarkGreen;
Console.ForegroundColor = ConsoleColor.Black; Console.ForegroundColor = ConsoleColor.Black;
Console.Write(whitespaceOnly ? d.FormattedText : d.Text); text = whitespaceOnly ? d.FormattedText : d.Text;
Console.ResetColor();
break; break;
case Operation.Equal: case Operation.Equal:
Console.Write(d.Text); text = d.Text;
break; break;
default: default:
throw new ArgumentOutOfRangeException(d.Operation.ToString()); throw new ArgumentOutOfRangeException(d.Operation.ToString());
} }
Console.Write(text);
Console.ResetColor();
}
} }
} }

View File

@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="google-diff-match-patch" Version="1.3.70" /> <PackageReference Include="google-diff-match-patch" Version="1.3.74" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<ProjectReference Include="..\..\DTLib\DTLib\DTLib.csproj" /> <ProjectReference Include="..\..\DTLib\DTLib\DTLib.csproj" />

View File

@ -1,76 +1,247 @@
using System.Linq;
using DTLib.Console;
using DTLib.Dtsod;
using diff_text;
using DiffMatchPatch;
using DTLib.Ben.Demystifier;
namespace ParadoxModMerger; namespace ParadoxModMerger;
public record struct ConflictingModFile(string FilePath, string[] Mods);
public enum DiffState
{
Added, Equal, Removed, Changed
}
public record struct DiffPart<T>(T Value, DiffState State);
static class Diff static class Diff
{ {
static ConsoleLogger logger = new($"logs", "diff"); static ConsoleLogger logger = new($"logs", "diff");
static void Log(params string[] msg) => logger.Log(msg); static void Log(params string[] msg) => logger.Log(msg);
public static void DiffMods(string connectedPathes) public static void DiffCommandHandler(string connected_pathes)
{ {
string[] split = connectedPathes.Split(';'); IOPath[] moddirs = Program.SplitStringToPaths(connected_pathes);
DiffMods(split[0], split[1]); var conflicts = FindModConflicts(moddirs);
LogModConflicts(conflicts);
} }
public static void DiffMods(IOPath moddir0, IOPath moddir1) public static void DiffConflictsCommandHandler(IOPath conflicts_dtsod_path)
{ {
var hasher = new Hasher(); var dtsod = new DtsodV23(File.ReadAllText(conflicts_dtsod_path));
var diff = new Dictionary<IOPath, byte[]>(); var conflicts = new ConflictingModFile[dtsod.Count];
// добавление файлов из первой папки int i = 0;
List<IOPath> files = Directory.GetAllFiles(moddir0); foreach (var p in dtsod)
var mods = new List<IOPath>();
for (short i = 0; i < files.Count; i++)
{ {
byte[] hash = hasher.HashFile(files[i]); conflicts[i]=new ConflictingModFile(p.Key, ((List<object>)p.Value).Select(m=>(string)m).ToArray());
files[i] = files[i].ReplaceBase(moddir0, ""); i++;
diff.Add(files[i], hash); }
AddMod(files[i]); ShowConflictsTextDiff(conflicts);
} }
// убирание совпадающих файлов public static void ShowConflictsTextDiff(ConflictingModFile[] conflicts)
files = Directory.GetAllFiles(moddir1);
for (short i = 0; i < files.Count; i++)
{ {
byte[] hash = hasher.HashFile(files[i]); int selected_confl_i = 0;
files[i] = files[i].RemoveBase(moddir1); int selected_mod0_i = 0;
if (diff.ContainsKey(files[i]) && diff[files[i]].HashToString() == hash.HashToString()) int selected_mod1_i = 1;
diff.Remove(files[i]); int line_buffer_offset = 0;
else
int line_i=0;
for (int i = 0; i < conflicts.Length; i++)
line_i += 1 + conflicts[i].Mods.Length;
var lines = new (ConsoleColor color, string text)[line_i];
// set lines text
line_i = 0;
for (int confl_i = 0; confl_i < conflicts.Length; confl_i++)
{ {
diff.Add(Path.Concat(moddir1,files[i]), hash); lines[line_i].color = ConsoleColor.White;
AddMod(files[i]); lines[line_i++].text = $"[{confl_i}]{conflicts[confl_i].FilePath}";
for (int mod_i = 0; mod_i < conflicts[confl_i].Mods.Length; mod_i++)
{
lines[line_i].color = ConsoleColor.Gray;
lines[line_i++].text = $" [{mod_i}] {conflicts[confl_i].Mods[mod_i]}";
} }
} }
void AddMod(IOPath mod) while (true)
{ {
mod = mod.Remove(0, 1); try
mod = mod.Remove(mod.IndexOf(Path.Sep)); {
if (!mods.Contains(mod)) mods.Add(mod); // set line colors
} line_i = 0;
for (int confl_i = 0; confl_i < conflicts.Length; confl_i++)
{
if (confl_i == selected_confl_i)
lines[line_i].color = ConsoleColor.Blue;
else lines[line_i].color = ConsoleColor.White;
line_i++;
// вывод результата for (int mod_i = 0; mod_i < conflicts[confl_i].Mods.Length; mod_i++)
StringBuilder output = new StringBuilder();
output.Append($"[{DateTime.Now}]\n\n");
foreach (var mod in mods)
{ {
output.Append('\n').Append(mod).Append("\n{\n"); if (confl_i == selected_confl_i)
foreach (var file in diff.Keys)
{ {
if (file.Contains(mod)) if (mod_i == selected_mod0_i)
{ lines[line_i].color = ConsoleColor.Yellow;
output.Append('\t'); else if (mod_i == selected_mod1_i)
if (!file.Contains(moddir1)) output.Append(moddir0).Append(file).Append('\n'); lines[line_i].color = ConsoleColor.Green;
output.Append(file).Append('\n'); else lines[line_i].color = ConsoleColor.Gray;
}
else lines[line_i].color = ConsoleColor.Gray;
line_i++;
} }
} }
output.Append("}\n"); // print lines
// не убирать, это полезное Console.Clear();
if (output[output.Length - 4] == '{') ColoredConsole.WriteLine("c",
output.Remove(output.Length - mod.Length - 5, mod.Length + 5); "[Q]exit [Up/Down]select file [Left/Right][number]select mod [Enter]show diff");
for (int i = line_buffer_offset;
i < lines.Length && i < Console.WindowHeight + line_buffer_offset - 2;
i++)
ColoredConsole.WriteLine(lines[i].color, lines[i].text);
// read input
ConsoleKeyInfo key = Console.ReadKey();
switch (key.Key)
{
case ConsoleKey.Q:
return;
case ConsoleKey.UpArrow:
if (selected_confl_i > 0)
{
line_buffer_offset -= conflicts[selected_confl_i].Mods.Length;
line_buffer_offset--;
selected_confl_i--;
} }
var _outStr = output.ToString(); break;
Log("w", _outStr); case ConsoleKey.DownArrow:
if (selected_confl_i < conflicts.Length - 1)
{
selected_confl_i++;
line_buffer_offset++;
line_buffer_offset += conflicts[selected_confl_i].Mods.Length;
}
break;
case ConsoleKey.Enter:
{
var conflict = conflicts[selected_confl_i];
var path0 = Path.Concat(conflict.Mods[selected_mod0_i], conflict.FilePath);
var path1 = Path.Concat(conflict.Mods[selected_mod1_i], conflict.FilePath);
if (path0.Extension() == "dds")
{
ColoredConsole.Write("r", $"file {path0} is not text file\n",
"c", "press enter to continue: ");
Console.ReadLine();
}
var textDiff = DiffText.FileDiff(path0.Str, path1.Str);
Console.Clear();
DiffText.PrintDiff(textDiff, true);
ColoredConsole.Write("c", "\npress enter to continue: ");
Console.ReadLine();
break;
}
case ConsoleKey.LeftArrow:
{
ColoredConsole.Write("w", "enter left mod number: ");
string answ = Console.ReadLine();
selected_mod0_i = answ.ToInt();
break;
}
case ConsoleKey.RightArrow:
{
ColoredConsole.Write("w", "enter right mod number: ");
string answ = Console.ReadLine();
selected_mod1_i = answ.ToInt();
break;
}
}
}
catch (Exception ex)
{
Log("r",ex.ToStringDemystified());
ColoredConsole.Write("c", "\npress enter to continue: ");
Console.ReadLine();
}
}
}
public static ICollection<ConflictingModFile> FindModConflicts(IOPath[] modpaths)
{
var all_files = new Dictionary<string, List<string>>();
foreach (var modp in modpaths)
{
foreach (var _file in Directory.GetAllFiles(modp))
{
var file = _file.RemoveBase(modp);
if (all_files.TryGetValue(file.Str, out var associated_mods))
associated_mods.Add(modp.Str);
else all_files.Add(file.Str, new List<string>(1) { modp.Str });
}
}
var output = new List<ConflictingModFile>();
foreach (var p in all_files)
if (p.Value.Count > 1)
output.Add(new ConflictingModFile(p.Key, p.Value.ToArray()));
return output;
}
public static IEnumerable<DiffPart<T>> DiffCollections<T>(ICollection<T> col0, ICollection<T> col1)
{
foreach (T el in col0)
yield return new DiffPart<T>(el, col1.Contains(el) ? DiffState.Equal : DiffState.Removed);
foreach (var el in col1)
if (!col0.Contains(el))
yield return new DiffPart<T>(el, DiffState.Added);
}
public static IEnumerable<DiffPart<IOPath>> DiffDirs(IOPath dir0, IOPath dir1)
{
var files0 = Directory.GetAllFiles(dir0).Select(p=>p.RemoveBase(dir0)).ToList();
var files1 = Directory.GetAllFiles(dir1).Select(p=>p.RemoveBase(dir1)).ToList();
var filesMerged = DiffCollections(files0, files1);
foreach (var filePathDiff in filesMerged)
{
if (filePathDiff.State == DiffState.Equal)
{
Hasher hasher = new Hasher();
string hash0=hasher.HashFile(Path.Concat(dir0, filePathDiff.Value)).HashToString();
string hash1=hasher.HashFile(Path.Concat(dir1, filePathDiff.Value)).HashToString();
if (hash0 != hash1)
yield return filePathDiff with { State = DiffState.Changed };
else yield return filePathDiff;
}
else yield return filePathDiff;
}
}
// вывод конфликтующих файлов при -merge и -clear если такие есть
public static void LogModConflicts(ICollection<ConflictingModFile> conflicts)
{
if(conflicts.Count==0) return;
Log("m",$"found {conflicts.Count} conflicting files:");
var dtsod = new DtsodV23();
foreach (var cfl in conflicts)
{
Log("m", "file ","c", cfl.FilePath, "m", "in mods", "b", cfl.Mods.MergeToString(", "));
dtsod.Add(cfl.FilePath, cfl.Mods);
}
string timeStr = DateTime.Now.ToString(MyTimeFormat.ForFileNames);
IOPath conflicts_dtsod_path = Path.Concat("conflicts", $"conflicts_{timeStr}.dtsod");
File.WriteAllText(conflicts_dtsod_path, dtsod.ToString());
conflicts_dtsod_path = Path.Concat("conflicts", "conflicts_latest.dtsod");
File.WriteAllText(conflicts_dtsod_path, dtsod.ToString());
Log("m",$"conflicts have written to {conflicts_dtsod_path}");
} }
} }

View File

@ -1,4 +1,7 @@
namespace ParadoxModMerger; using System.Linq;
using DTLib.Console;
namespace ParadoxModMerger;
static class Merge static class Merge
{ {
@ -8,17 +11,43 @@ static class Merge
public static void MergeAll(IOPath[] moddirs, IOPath outDir) public static void MergeAll(IOPath[] moddirs, IOPath outDir)
{ {
Log("b", $"found {moddirs.Length} mod dirs"); Log("b", $"found {moddirs.Length} mod dirs");
HandleConflicts(moddirs);
for (short i = 0; i < moddirs.Length; i++) for (short i = 0; i < moddirs.Length; i++)
{ {
Log("b", $"[{i + 1}/{moddirs.Length}] merging mod ", "c", $"{moddirs[i]}"); Log("b", $"[{i + 1}/{moddirs.Length}] merging mod ", "c", $"{moddirs[i]}");
Directory.Copy(moddirs[i], outDir, true, out var _conflicts); Directory.Copy(moddirs[i], outDir, true);
Program.LogConflicts(_conflicts);
} }
} }
public static void MergeSingle(IOPath moddir, IOPath outDir) public static void MergeInto(IOPath moddir, IOPath outDir)
{ {
Directory.Copy(moddir, outDir, true, out var _conflicts); HandleConflicts(new[] { moddir, outDir });
Program.LogConflicts(_conflicts); Directory.Copy(moddir, outDir, true);
}
public static void ConsoleAskYN(string question, Action yes, Action no)
{
Log("y", question + " [y/n]");
string answ = ColoredConsole.Read("w");
if (answ == "y") yes();
else no();
}
static void HandleConflicts(IOPath[] moddirs)
{
var conflicts = Diff.FindModConflicts(moddirs);
if (conflicts.Count <= 0) return;
Diff.LogModConflicts(conflicts);
ConsoleAskYN("continue merge?",
() =>Log("y", "merge continued"),
() =>
{
Log("y", "merge interrupted");
ConsoleAskYN("show text diff?",
() => Diff.ShowConflictsTextDiff(conflicts.ToArray()),
() => {});
});
} }
} }

View File

@ -36,26 +36,36 @@ public static class Program
"workshop_dir", "workshop_dir",
1), 1),
new LaunchArgument(new []{"diff"}, new LaunchArgument(new []{"diff"},
"Compare mod files by hash", "Compares mod files by hash",
p=>Diff.DiffMods(p), p=>Diff.DiffCommandHandler(p),
"first_mod_directory;second_mod_directory", 1), "first_mod_directory:second_mod_directory:...",
1),
new LaunchArgument(new []{"diff-conflicts"},
"reads conflicts_XXX.dtsod file and shows text diff for each file",
p=>Diff.DiffConflictsCommandHandler(p),
"conflicts_dtsod_path",
1
),
new LaunchArgument(new []{"merge-subdirs"}, new LaunchArgument(new []{"merge-subdirs"},
"Merge mods and show conflicts. Requires -o", "Merges mods and shows conflicts. Requires -o",
d => Merge.MergeAll(Directory.GetDirectories(d), outPath), d => Merge.MergeAll(Directory.GetDirectories(d), outPath),
"dir_with_mods", "dir_with_mods",
1), 1),
new LaunchArgument(new []{"merge-single"}, new LaunchArgument(new []{"merge-into", "merge-single"},
"Merges one mod into output dir and shows conflicts. Requires -o", "Merges one mod into output dir and shows conflicts. Requires -o",
mod=>Merge.MergeSingle(mod, outPath), mod=>Merge.MergeInto(mod, outPath),
"mod_dir", "mod_dir",
1), 1),
new LaunchArgument(new []{"gen-rus-locale"}, new LaunchArgument(new []{"gen-rus-locale"},
"Creates l_russian copy of english locale in output directory. Requires -o", "Creates l_russian copy of english locale in output directory. Requires -o",
eng=>Localisation.GenerateRussian(eng, outPath), eng=>Localisation.GenerateRussian(eng, outPath),
"english_locale_path", 1), "english_locale_path",
1),
new LaunchArgument(new []{"desc"}, new LaunchArgument(new []{"desc"},
"Downloads mod description from steam to new file in outDir. Requires -o", "Downloads mod description from steam to new file in outDir. Requires -o",
id=>Workshop.CreateDescFile(id, outPath), "mod_id") id=>Workshop.CreateDescFile(id, outPath),
"mod_id",
1)
).ParseAndHandle(args); ).ParseAndHandle(args);
} }
catch (LaunchArgumentParser.ExitAfterHelpException) catch (LaunchArgumentParser.ExitAfterHelpException)
@ -67,11 +77,14 @@ public static class Program
Console.ResetColor(); Console.ResetColor();
} }
public static IOPath[] SplitStringToPaths(string connected_paths)
// вывод конфликтующих файлов при -merge и -clear если такие есть
public static void LogConflicts(List<IOPath> conflicts)
{ {
if(conflicts.Count>0) if (!connected_paths.Contains(':'))
Log("y", $"conflicts found: {conflicts.Count}\n{conflicts.MergeToString("\n")}"); throw new Exception($"<{connected_paths}> doesn't contain any separators (:)");
string[] split = connected_paths.Split(':');
IOPath[] split_iop = new IOPath[split.Length];
for (int i = 0; i < split.Length; i++)
split_iop[i] = new IOPath(split[i]);
return split_iop;
} }
} }

View File

@ -78,7 +78,7 @@ static class Workshop
Directory.Copy(Path.Concat(moddirs[i], subdirs[n]), Directory.Copy(Path.Concat(moddirs[i], subdirs[n]),
Path.Concat(outModDir, subdirs[n]), Path.Concat(outModDir, subdirs[n]),
true, out var _conflicts); true, out var _conflicts);
Program.LogConflicts(_conflicts); Log("y", $"found {_conflicts.Count} conflicts:\n{_conflicts.MergeToString('\n')}");
break; break;
} }
} }

View File

@ -11,7 +11,8 @@
<None Include="7z\**" CopyToOutputDirectory="PreserveNewest" /> <None Include="7z\**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DTLib.Ben.Demystifier" Version="1.0.3" /> <PackageReference Include="DTLib.Ben.Demystifier" Version="1.0.4" />
<PackageReference Include="DTLib.Dtsod" Version="1.1.4" />
<PackageReference Include="Fizzler.Systems.HtmlAgilityPack" Version="1.2.1" /> <PackageReference Include="Fizzler.Systems.HtmlAgilityPack" Version="1.2.1" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
@ -20,4 +21,7 @@
<ItemGroup Condition=" '$(Configuration)' != 'Debug' "> <ItemGroup Condition=" '$(Configuration)' != 'Debug' ">
<PackageReference Include="DTLib" Version="1.1.4" /> <PackageReference Include="DTLib" Version="1.1.4" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\diff-text\diff-text.csproj" />
</ItemGroup>
</Project> </Project>

4
publish_debug.sh Normal file
View File

@ -0,0 +1,4 @@
#!/bin/sh
rm -rf publish
mkdir publish
dotnet publish -c debug -o publish -f net7.0