5467de0d1a
- Introduced `ChangeVersion` method for version manipulation based on major, minor, and patch increments. - Updated .gitignore to include `obj/` directory. - Added `Aigamo.ResXGenerator` dependency and configuration for ResX generation. - Refactored argument parsing in `Program.cs` for improved readability and error handling. - Cleaned up `Exceptions.resx` file format and extended comments for clarity.
269 lines
9.9 KiB
C#
269 lines
9.9 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using System.Xml;
|
|
using aeqw89.xml.ProjectFile;
|
|
|
|
namespace aeqw89.tools.Publish;
|
|
|
|
// -------------------------------------------------------------------------------------------------
|
|
// Minimal replacements for VsTools.Projects so ProjectFile can keep its public surface intact
|
|
// -------------------------------------------------------------------------------------------------
|
|
|
|
// -------------------------------------------------------------------------------------------------
|
|
// Your original public surface, now backed by System.Xml
|
|
// -------------------------------------------------------------------------------------------------
|
|
|
|
internal class ProjectFile {
|
|
public Project Project { get; private set; }
|
|
public string Path { get; private set; }
|
|
private PropertyGroup MainPropertyGroup { get; set; }
|
|
|
|
private ProjectFile(Project project, string path) {
|
|
MainPropertyGroup = project.PropertyGroups.FirstOrDefault()!;
|
|
Project = project;
|
|
Path = path;
|
|
}
|
|
|
|
public static bool TryLoad(string path, [NotNullWhen(true)] out ProjectFile? projectFile, [NotNullWhen(false)] out string? error) {
|
|
projectFile = null;
|
|
error = null;
|
|
|
|
if (!path.EndsWith(".csproj")) {
|
|
if (!Directory.Exists(path)) {
|
|
error = Exceptions.tried_loading_non_csproj_file;
|
|
return false;
|
|
}
|
|
|
|
var csproj = Directory.EnumerateFiles(path, "*.csproj", SearchOption.TopDirectoryOnly).ToArray();
|
|
if (csproj.Length == 0) {
|
|
error = Exceptions.no_project_in_directory;
|
|
return false;
|
|
}
|
|
|
|
if (csproj.Length > 1) {
|
|
error = Exceptions.found_multiple_csproj;
|
|
return false;
|
|
}
|
|
|
|
path = csproj[0];
|
|
}
|
|
|
|
try {
|
|
projectFile = new ProjectFile(Project.Load(path), path);
|
|
return true;
|
|
}
|
|
catch (Exception e) {
|
|
error = string.Format(Exceptions.generic_error, e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public void Backup(string? backupPath = null) {
|
|
backupPath ??= Path + ".bak";
|
|
File.Copy(Path, backupPath, true);
|
|
}
|
|
|
|
public string GetDefaultBackupLocation() => Path + ".bak";
|
|
|
|
public bool Restore(string? backupPath = null) {
|
|
backupPath ??= GetDefaultBackupLocation();
|
|
if (!File.Exists(backupPath)) return false;
|
|
File.Copy(backupPath, Path, true);
|
|
return true;
|
|
}
|
|
|
|
public void Save() {
|
|
// Check for empty property or item groups
|
|
foreach (var ig in Project.ItemGroups.ToList().Where(ig => ig.Items.Count == 0)) {
|
|
ig.Remove();
|
|
}
|
|
foreach (var pg in Project.PropertyGroups.ToList().Where(pg => pg.Count == 0)) {
|
|
pg.Remove();
|
|
}
|
|
|
|
Project.Save();
|
|
}
|
|
|
|
public bool TryRepair([NotNullWhen(false)] out string? error) {
|
|
// Ensure we have a PropertyGroup with TargetFramework
|
|
if (MainPropertyGroup?.HasProperty("TargetFramework") != true) {
|
|
if (!Project.PropertyGroups.Any(x => x.HasProperty("TargetFramework"))) {
|
|
error = string.Format(Exceptions.project_file_irreparable, Path, "TargetFramework");
|
|
return false;
|
|
}
|
|
MainPropertyGroup = Project.PropertyGroups.First(x => x.HasProperty("TargetFramework"));
|
|
}
|
|
|
|
List<string> failed = [];
|
|
|
|
void set(string key, string value, bool required = false) {
|
|
if (!MainPropertyGroup.HasProperty(key) || string.IsNullOrEmpty(MainPropertyGroup.GetProperty(key))) {
|
|
if (required) {
|
|
failed.Add(key);
|
|
return;
|
|
}
|
|
MainPropertyGroup.SetProperty(key, value);
|
|
}
|
|
}
|
|
|
|
set("Version", "1.0.0");
|
|
set("PackageVersion", "1.0.0");
|
|
set("Title", System.IO.Path.GetFileNameWithoutExtension(Path));
|
|
set("Authors", "");
|
|
set("Company", "");
|
|
set("Description", "");
|
|
set("PackageProjectUrl", "", required: false);
|
|
set("RepositoryUrl", "", required: true);
|
|
set("PackageId", "", required: true);
|
|
|
|
if (failed.Count > 0) {
|
|
error = string.Format(Exceptions.project_file_irreparable, Path, string.Join(", ", failed));
|
|
return false;
|
|
}
|
|
|
|
error = null;
|
|
return true;
|
|
}
|
|
|
|
public List<PackageReference> GetPackageReferences() {
|
|
return Project.ItemGroups
|
|
.SelectMany(g => g.Items)
|
|
.Where(i => i.ElementName == "PackageReference")
|
|
.OfType<PackageReference>()
|
|
.ToList();
|
|
}
|
|
|
|
public List<ProjectReference> GetProjectReferences() {
|
|
return Project.ItemGroups
|
|
.SelectMany(g => g.Items)
|
|
.Where(i => i.ElementName == "ProjectReference")
|
|
.OfType<ProjectReference>()
|
|
.ToList();
|
|
}
|
|
|
|
public List<Content> GetPackageDependencies() {
|
|
return Project.ItemGroups
|
|
.SelectMany(g => g.Items)
|
|
.Where(i => i.ElementName == "Content")
|
|
.OfType<Content>()
|
|
.ToList();
|
|
}
|
|
|
|
private void CreateOrUpdateChild(Item item, string childName, string value) {
|
|
var child = item.Node.SelectSingleNode(childName) as XmlElement;
|
|
if (child != null) {
|
|
child.InnerText = value;
|
|
return;
|
|
}
|
|
|
|
child = item.Node.OwnerDocument!.CreateElement(childName, item.Node.NamespaceURI);
|
|
child.InnerText = value;
|
|
item.Node.AppendChild(child);
|
|
}
|
|
|
|
public string GetPackageId() {
|
|
return MainPropertyGroup.GetProperty("PackageId");
|
|
}
|
|
|
|
public void SetPrivateAssets(Item item, PrivateAssetsValue value) {
|
|
if (value == PrivateAssetsValue.None) {
|
|
var child = item.Node.SelectSingleNode("./PrivateAssets") as XmlElement;
|
|
if (child != null)
|
|
item.Node.RemoveChild(child);
|
|
return;
|
|
}
|
|
|
|
CreateOrUpdateChild(item, "PrivateAssets", value.ToString().ToLowerInvariant());
|
|
}
|
|
|
|
public void SetTransitive(Item item, bool value)
|
|
=> CreateOrUpdateChild(item, "Transitive", value.ToString().ToLowerInvariant());
|
|
|
|
public bool IsTransitive(Item item) {
|
|
var transitive = item.Node.SelectSingleNode("./Transitive") as XmlElement;
|
|
return transitive?.InnerText == "true";
|
|
}
|
|
|
|
public string GetAbsoluteIncludePath(Item item) {
|
|
return System.IO.Path.GetFullPath(item.Include);
|
|
}
|
|
|
|
public Item AddPackage(Item otherPackage) {
|
|
var allItems = Project.ItemGroups.SelectMany(x => x.Items);
|
|
var existing = allItems.FirstOrDefault(x =>
|
|
x.ElementName == "PackageReference" && x.Include == otherPackage.Include);
|
|
if (existing != null) return existing;
|
|
|
|
var itemGroup = Project.ItemGroups.First();
|
|
// Import the original XmlElement to preserve Version (attr or child) and any metadata
|
|
itemGroup.Add(otherPackage);
|
|
return itemGroup.Items.Last();
|
|
}
|
|
|
|
|
|
public bool RemovePackage(Item otherPackage) {
|
|
var grp = Project.ItemGroups
|
|
.FirstOrDefault(g => g.Items.Any(y => y.ElementName == "PackageReference" && y.Include == otherPackage.Include));
|
|
|
|
var pkg = grp?.Items.FirstOrDefault(x => x.ElementName == otherPackage.ElementName && x.Include == otherPackage.Include);
|
|
if (pkg == null) return false;
|
|
|
|
pkg.Node.ParentNode!.RemoveChild(pkg.Node);
|
|
grp!.Items.Remove(pkg);
|
|
return true;
|
|
}
|
|
|
|
public string GetVersion() => MainPropertyGroup.GetProperty("Version");
|
|
|
|
public void SetVersion(string version) {
|
|
MainPropertyGroup.SetProperty("Version", version);
|
|
MainPropertyGroup.SetProperty("PackageVersion", version);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the version string by applying the specified operation to the major, minor, and patch components of the version.
|
|
/// </summary>
|
|
/// <param name="version">The current version string in the format "major.minor.patch[-tag]".</param>
|
|
/// <param name="patch">The value to apply to the patch component.</param>
|
|
/// <param name="minor">The value to apply to the minor component.</param>
|
|
/// <param name="major">The value to apply to the major component.</param>
|
|
/// <param name="operation">A function that defines the adjustment operation to be performed on each version component.</param>
|
|
/// <returns>A new version string with the updated major, minor, and patch components, preserving any existing tag.</returns>
|
|
/// <exception cref="Exception">Thrown if the version string is not in the correct format.</exception>
|
|
public static Result<string, ReadableError> ChangeVersion(string version, int delta, IncrementTarget target) {
|
|
string[] split = version.Split('.');
|
|
if (split.Length != 3) {
|
|
return new ReadableError(string.Format(Exceptions.version_string_not_formatted_correctly, version));
|
|
}
|
|
|
|
string tag = "";
|
|
if (split[2].Contains('-')) {
|
|
var split2 = split[2].Split('-');
|
|
split[2] = split2[0];
|
|
tag = "-" + split2[1];
|
|
}
|
|
|
|
if (split.Any(x => !int.TryParse(x, out _)))
|
|
return new ReadableError(string.Format(Exceptions.version_string_not_formatted_correctly, version));
|
|
|
|
int[] parsedVersion = split.Select(int.Parse).ToArray();
|
|
switch (target) {
|
|
case IncrementTarget.Major:
|
|
parsedVersion[0] += delta;
|
|
parsedVersion[1] = 0;
|
|
parsedVersion[2] = 0;
|
|
break;
|
|
case IncrementTarget.Minor:
|
|
parsedVersion[1] += delta;
|
|
parsedVersion[2] = 0;
|
|
break;
|
|
case IncrementTarget.Patch:
|
|
parsedVersion[2] += delta;
|
|
break;
|
|
}
|
|
|
|
return
|
|
$"{parsedVersion[0]}.{parsedVersion[1]}.{parsedVersion[2]}{tag}".Ok();
|
|
}
|
|
|
|
}
|