2020-04-06 12:04:40 +00:00
|
|
|
|
using System;
|
2020-04-06 18:26:09 +00:00
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Text;
|
2020-04-06 12:04:40 +00:00
|
|
|
|
using System.Threading.Tasks;
|
2020-04-06 18:26:09 +00:00
|
|
|
|
using Alphaleonis.Win32.Filesystem;
|
2020-04-06 12:04:40 +00:00
|
|
|
|
using CommandLine;
|
2020-04-06 18:26:09 +00:00
|
|
|
|
using Markdig;
|
|
|
|
|
using Markdig.Syntax;
|
|
|
|
|
using Wabbajack.Common;
|
|
|
|
|
using Wabbajack.Lib;
|
|
|
|
|
using Wabbajack.Lib.Downloaders;
|
2020-04-06 12:04:40 +00:00
|
|
|
|
|
|
|
|
|
namespace Wabbajack.CLI.Verbs
|
|
|
|
|
{
|
|
|
|
|
[Verb("changelog", HelpText = "Generate a changelog using two different versions of the same Modlist.")]
|
|
|
|
|
public class Changelog : AVerb
|
|
|
|
|
{
|
2020-04-09 22:36:07 +00:00
|
|
|
|
[IsFile(CustomMessage = "Modlist %1 does not exist!", Extension = Consts.ModListExtensionString)]
|
2020-04-06 12:04:40 +00:00
|
|
|
|
[Option("original", Required = true, HelpText = "The original/previous modlist")]
|
2020-04-09 22:36:07 +00:00
|
|
|
|
public string Original { get; set; } = "";
|
2020-04-06 12:04:40 +00:00
|
|
|
|
|
2020-04-09 22:36:07 +00:00
|
|
|
|
[IsFile(CustomMessage = "Modlist %1 does not exist!", Extension = Consts.ModListExtensionString)]
|
2020-04-06 12:04:40 +00:00
|
|
|
|
[Option("update", Required = true, HelpText = "The current/updated modlist")]
|
2020-04-09 22:36:07 +00:00
|
|
|
|
public string Update { get; set; } = "";
|
2020-04-06 12:04:40 +00:00
|
|
|
|
|
|
|
|
|
[Option('o', "output", Required = false, HelpText = "The output file")]
|
|
|
|
|
public string? Output { get; set; }
|
2020-04-06 18:26:09 +00:00
|
|
|
|
|
|
|
|
|
[Option("changes-downloads", Required = false, Default = true, HelpText = "Include download changes")]
|
|
|
|
|
public bool IncludeDownloadChanges { get; set; }
|
|
|
|
|
|
|
|
|
|
[Option("changes-mods", Required = false, Default = false, HelpText = "Include mods changes")]
|
|
|
|
|
public bool IncludeModChanges { get; set; }
|
|
|
|
|
|
|
|
|
|
[Option("changes-loadorder", Required = false, Default = false, HelpText = "Include load order changes")]
|
|
|
|
|
public bool IncludeLoadOrderChanges { get; set; }
|
2020-04-06 12:04:40 +00:00
|
|
|
|
|
2020-04-06 18:26:09 +00:00
|
|
|
|
protected override async Task<ExitCode> Run()
|
2020-04-06 12:04:40 +00:00
|
|
|
|
{
|
2020-04-16 15:50:06 +00:00
|
|
|
|
var originalPath = (AbsolutePath)Original;
|
2020-04-09 22:36:07 +00:00
|
|
|
|
var updatePath = (AbsolutePath)Update;
|
2020-04-06 18:26:09 +00:00
|
|
|
|
if (Original == null)
|
|
|
|
|
return ExitCode.BadArguments;
|
|
|
|
|
if (Update == null)
|
|
|
|
|
return ExitCode.BadArguments;
|
|
|
|
|
|
|
|
|
|
ModList original, update;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2020-04-16 15:50:06 +00:00
|
|
|
|
original = AInstaller.LoadFromFile(originalPath);
|
2020-04-06 18:26:09 +00:00
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
return CLIUtils.Exit($"Error while loading the original Modlist from {Original}!\n{e}", ExitCode.Error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(original == null)
|
|
|
|
|
return CLIUtils.Exit($"The Modlist from {Original} could not be loaded!", ExitCode.Error);
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2020-04-09 22:36:07 +00:00
|
|
|
|
update = AInstaller.LoadFromFile(updatePath);
|
2020-04-06 18:26:09 +00:00
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
return CLIUtils.Exit($"Error while loading the updated Modlist from {Update}!\n{e}", ExitCode.Error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(update == null)
|
|
|
|
|
return CLIUtils.Exit($"The Modlist from {Update} could not be loaded!", ExitCode.Error);
|
|
|
|
|
|
|
|
|
|
var downloadSizeChanges = original.DownloadSize - update.DownloadSize;
|
|
|
|
|
var installSizeChanges = original.InstallSize - update.InstallSize;
|
|
|
|
|
|
|
|
|
|
var mdText =
|
2020-04-16 15:50:06 +00:00
|
|
|
|
$"## {update.Version}\n\n" +
|
2020-04-06 18:26:09 +00:00
|
|
|
|
$"**Build at:** `{File.GetCreationTime(Update)}`\n\n" +
|
|
|
|
|
"**Info**:\n\n" +
|
|
|
|
|
$"- Download Size change: {downloadSizeChanges.ToFileSizeString()} (Total: {update.DownloadSize.ToFileSizeString()})\n" +
|
|
|
|
|
$"- Install Size change: {installSizeChanges.ToFileSizeString()} (Total: {update.InstallSize.ToFileSizeString()})\n\n";
|
|
|
|
|
|
|
|
|
|
if (IncludeDownloadChanges)
|
|
|
|
|
{
|
|
|
|
|
var updatedArchives = update.Archives
|
|
|
|
|
.Where(a => original.Archives.All(x => x.Name != a.Name))
|
|
|
|
|
.Where(a =>
|
|
|
|
|
{
|
|
|
|
|
if (!(a.State is NexusDownloader.State nexusState))
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
return original.Archives.Any(x =>
|
|
|
|
|
{
|
|
|
|
|
if (!(x.State is NexusDownloader.State originalState))
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
if (nexusState.Name != originalState.Name)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
if (nexusState.ModID != originalState.ModID)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
2020-04-09 22:36:07 +00:00
|
|
|
|
return nexusState.FileID > originalState.FileID;
|
2020-04-06 18:26:09 +00:00
|
|
|
|
});
|
|
|
|
|
}).ToList();
|
|
|
|
|
|
|
|
|
|
var newArchives = update.Archives
|
|
|
|
|
.Where(a => original.Archives.All(x => x.Name != a.Name))
|
|
|
|
|
.Where(a => updatedArchives.All(x => x != a))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
var removedArchives = original.Archives
|
|
|
|
|
.Where(a => update.Archives.All(x => x.Name != a.Name))
|
|
|
|
|
.Where(a => updatedArchives.All(x => x != a))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
if(newArchives.Any() || removedArchives.Any())
|
|
|
|
|
mdText += "**Download Changes**:\n\n";
|
|
|
|
|
|
|
|
|
|
updatedArchives.Do(a =>
|
|
|
|
|
{
|
|
|
|
|
mdText += $"- Updated [{GetModName(a)}]({a.State.GetManifestURL(a)})\n";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
removedArchives.Do(a =>
|
|
|
|
|
{
|
|
|
|
|
mdText += $"- Removed [{GetModName(a)}]({a.State.GetManifestURL(a)})\n";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
newArchives.Do(a =>
|
|
|
|
|
{
|
|
|
|
|
mdText += $"- Added [{GetModName(a)}]({a.State.GetManifestURL(a)})\n";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
mdText += "\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (IncludeLoadOrderChanges)
|
|
|
|
|
{
|
2020-04-16 15:50:06 +00:00
|
|
|
|
var loadorderTxt = (RelativePath)"loadorder.txt";
|
2020-04-06 18:26:09 +00:00
|
|
|
|
var originalLoadOrderFile = original.Directives
|
|
|
|
|
.Where(d => d is InlineFile)
|
2020-04-16 15:50:06 +00:00
|
|
|
|
.Where(d => d.To.FileName == loadorderTxt)
|
2020-04-06 18:26:09 +00:00
|
|
|
|
.Cast<InlineFile>()
|
|
|
|
|
.First();
|
|
|
|
|
|
|
|
|
|
var updatedLoadOrderFile = update.Directives
|
|
|
|
|
.Where(d => d is InlineFile)
|
2020-04-16 15:50:06 +00:00
|
|
|
|
.Where(d => d.To.FileName == loadorderTxt)
|
2020-04-06 18:26:09 +00:00
|
|
|
|
.Cast<InlineFile>()
|
|
|
|
|
.First();
|
|
|
|
|
|
2020-04-16 15:50:06 +00:00
|
|
|
|
var originalLoadOrder = GetTextFileFromModlist(originalPath, original, originalLoadOrderFile.SourceDataID).Result.Split("\n");
|
2020-04-09 22:36:07 +00:00
|
|
|
|
var updatedLoadOrder = GetTextFileFromModlist(updatePath, update, updatedLoadOrderFile.SourceDataID).Result.Split("\n");
|
2020-04-06 18:26:09 +00:00
|
|
|
|
|
|
|
|
|
var addedPlugins = updatedLoadOrder
|
|
|
|
|
.Where(p => originalLoadOrder.All(x => p != x))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
var removedPlugins = originalLoadOrder
|
|
|
|
|
.Where(p => updatedLoadOrder.All(x => p != x))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
if(addedPlugins.Any() || removedPlugins.Any())
|
|
|
|
|
mdText += "**Load Order Changes**:\n\n";
|
|
|
|
|
|
|
|
|
|
addedPlugins.Do(p =>
|
|
|
|
|
{
|
|
|
|
|
mdText += $"- Added {p}\n";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
removedPlugins.Do(p =>
|
|
|
|
|
{
|
|
|
|
|
mdText += $"- Removed {p}\n";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
mdText += "\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (IncludeModChanges)
|
|
|
|
|
{
|
2020-04-09 22:36:07 +00:00
|
|
|
|
var modlistTxt = (RelativePath)"modlist.txt";
|
2020-04-06 18:26:09 +00:00
|
|
|
|
var originalModlistFile = original.Directives
|
|
|
|
|
.Where(d => d is InlineFile)
|
2020-04-09 22:36:07 +00:00
|
|
|
|
.Where(d => d.To.FileName == modlistTxt)
|
2020-04-06 18:26:09 +00:00
|
|
|
|
.Cast<InlineFile>()
|
|
|
|
|
.First();
|
|
|
|
|
|
|
|
|
|
var updatedModlistFile = update.Directives
|
|
|
|
|
.Where(d => d is InlineFile)
|
2020-04-09 22:36:07 +00:00
|
|
|
|
.Where(d => d.To.FileName == modlistTxt)
|
2020-04-06 18:26:09 +00:00
|
|
|
|
.Cast<InlineFile>()
|
|
|
|
|
.First();
|
|
|
|
|
|
2020-04-16 15:50:06 +00:00
|
|
|
|
var originalModlist = GetTextFileFromModlist(originalPath, original, originalModlistFile.SourceDataID).Result.Split("\n");
|
2020-04-09 22:36:07 +00:00
|
|
|
|
var updatedModlist = GetTextFileFromModlist(updatePath, update, updatedModlistFile.SourceDataID).Result.Split("\n");
|
2020-04-06 18:26:09 +00:00
|
|
|
|
|
|
|
|
|
var removedMods = originalModlist
|
|
|
|
|
.Where(m => m.StartsWith("+"))
|
|
|
|
|
.Where(m => updatedModlist.All(x => m != x))
|
|
|
|
|
.Select(m => m.Substring(1))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
var addedMods = updatedModlist
|
|
|
|
|
.Where(m => m.StartsWith("+"))
|
|
|
|
|
.Where(m => originalModlist.All(x => m != x))
|
|
|
|
|
.Select(m => m.Substring(1))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
if (removedMods.Any() || addedMods.Any())
|
|
|
|
|
mdText += "**Mod Changes**:\n\n";
|
|
|
|
|
|
|
|
|
|
addedMods.Do(m =>
|
|
|
|
|
{
|
|
|
|
|
mdText += $"- Added {m}\n";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
removedMods.Do(m =>
|
|
|
|
|
{
|
|
|
|
|
mdText += $"- Removed {m}\n";
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var output = string.IsNullOrWhiteSpace(Output)
|
|
|
|
|
? "changelog.md"
|
|
|
|
|
: Output;
|
|
|
|
|
|
|
|
|
|
if (File.Exists(output) && output.EndsWith("md"))
|
|
|
|
|
{
|
|
|
|
|
CLIUtils.Log($"Output file {output} already exists and is a markdown file. It will be updated with the newest version");
|
|
|
|
|
|
|
|
|
|
var markdown = File.ReadAllLines(output).ToList();
|
|
|
|
|
var lines = mdText.Split("\n");
|
|
|
|
|
|
|
|
|
|
if (lines.All(l => markdown.Contains(l)))
|
|
|
|
|
{
|
|
|
|
|
return CLIUtils.Exit("The output file is already up-to-date", ExitCode.Ok);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var doc = Markdown.Parse(File.ReadAllText(output));
|
|
|
|
|
|
|
|
|
|
var hasToc = false;
|
|
|
|
|
var tocLine = 0;
|
|
|
|
|
|
|
|
|
|
var headers = doc
|
|
|
|
|
.Where(b => b is HeadingBlock)
|
|
|
|
|
.Cast<HeadingBlock>()
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
if (headers.Count < 2)
|
|
|
|
|
{
|
|
|
|
|
return CLIUtils.Exit("The provided output file has less than 2 headers!", ExitCode.Error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (headers[0].Level == 1 && headers[1].Level == 2)
|
|
|
|
|
{
|
|
|
|
|
if (headers[1].Line - headers[0].Line > headers.Count - 1)
|
|
|
|
|
{
|
|
|
|
|
var listBlocks = doc
|
|
|
|
|
.Where(b => b.Line > headers[0].Line && b.Line < headers[1].Line)
|
|
|
|
|
.OfType<ListBlock>()
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
if (listBlocks.Count == 1)
|
|
|
|
|
{
|
|
|
|
|
hasToc = true;
|
|
|
|
|
tocLine = listBlocks[0].Line;
|
|
|
|
|
|
|
|
|
|
CLIUtils.Log($"Toc found at {tocLine}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var firstHeader = headers
|
|
|
|
|
.First(h => h.Level >= 2);
|
|
|
|
|
|
|
|
|
|
var line = firstHeader.Line-1;
|
|
|
|
|
|
|
|
|
|
if (hasToc)
|
|
|
|
|
{
|
2020-04-16 15:50:06 +00:00
|
|
|
|
markdown.Insert(tocLine, $"- [{update.Version}](#{ToTocLink(update.Version.ToString())})");
|
2020-04-06 18:26:09 +00:00
|
|
|
|
line++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
markdown.InsertRange(line+1, lines);
|
|
|
|
|
|
|
|
|
|
File.WriteAllLines(output, markdown);
|
|
|
|
|
CLIUtils.Log($"Wrote {markdown.Count} lines to {output}");
|
|
|
|
|
|
|
|
|
|
return ExitCode.Ok;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var text = "# Changelog\n\n" +
|
2020-04-16 15:50:06 +00:00
|
|
|
|
$"- [{update.Version}](#{ToTocLink(update.Version.ToString())})\n\n" +
|
2020-04-06 18:26:09 +00:00
|
|
|
|
$"{mdText}";
|
|
|
|
|
|
|
|
|
|
File.WriteAllText(output, text);
|
|
|
|
|
CLIUtils.Log($"Wrote changelog to {output}");
|
|
|
|
|
|
|
|
|
|
return ExitCode.Ok;
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-09 22:36:07 +00:00
|
|
|
|
private static async Task<string> GetTextFileFromModlist(AbsolutePath archive, ModList modlist, RelativePath sourceID)
|
2020-04-06 18:26:09 +00:00
|
|
|
|
{
|
2020-04-10 01:29:53 +00:00
|
|
|
|
var installer = new MO2Installer(archive, modlist, default, default, parameters: null!);
|
2020-04-09 22:36:07 +00:00
|
|
|
|
byte[] bytes = await installer.LoadBytesFromPath(sourceID);
|
2020-04-06 18:26:09 +00:00
|
|
|
|
return Encoding.Default.GetString(bytes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string ToTocLink(string header)
|
|
|
|
|
{
|
|
|
|
|
return header.Trim().Replace(" ", "").Replace(".", "");
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-10 01:29:53 +00:00
|
|
|
|
private static string? GetModName(Archive a)
|
2020-04-06 18:26:09 +00:00
|
|
|
|
{
|
2020-04-10 01:29:53 +00:00
|
|
|
|
string? result = a.Name;
|
2020-04-06 18:26:09 +00:00
|
|
|
|
|
|
|
|
|
if (a.State is IMetaState metaState)
|
|
|
|
|
{
|
|
|
|
|
result = metaState.Name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
2020-04-06 12:04:40 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|