Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions dotnet/src/Devolutions.Pinget.Cli/CommonOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.CommandLine;
using Devolutions.Pinget.Cli.Extensions;

namespace Devolutions.Pinget.Cli;

internal static class CommonOptions
{
internal static Option<string?> Query => new Option<string?>("--query", "Query").WithAliases("-q");

internal static Option<string?> Id => new("--id", "Filter by id");

internal static Option<string?> Name => new("--name", "Filter by name");

internal static Option<string?> Moniker => new("--moniker", "Filter by moniker");

internal static Option<string?> Source => new Option<string?>("--source", "Source name").WithAliases("-s");

internal static Option<bool> Exact => new Option<bool>("--exact", "Exact match").WithAliases("-e");

internal static Option<int?> Count => new Option<int?>("--count", "Max results").WithAliases("-n");

internal static Option<string?> Version => new Option<string?>("--version", "Version").WithAliases("-v");
}
7 changes: 7 additions & 0 deletions dotnet/src/Devolutions.Pinget.Cli/Consts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Devolutions.Pinget.Cli;

internal static class Consts
{
internal const string Version = "0.4.2";
internal const string UpgradeUnsupportedWarning = "Upgrading packages is not supported on this platform; no changes were made.";
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,7 @@
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>

<Target Name="UseStaticWindowsRuntimeForNativeAot"
AfterTargets="SetupOSSpecificProps"
Condition="'$(PublishAot)' == 'true'
and '$(PingetUseStaticWindowsRuntime)' == 'true'
and $([System.String]::Copy('$(RuntimeIdentifier)').StartsWith('win-'))">
<Target Name="UseStaticWindowsRuntimeForNativeAot" AfterTargets="SetupOSSpecificProps" Condition="'$(PublishAot)' == 'true'&#xD;&#xA; and '$(PingetUseStaticWindowsRuntime)' == 'true'&#xD;&#xA; and $([System.String]::Copy('$(RuntimeIdentifier)').StartsWith('win-'))">
<ItemGroup>
<LinkerArg Remove="/NODEFAULTLIB:libucrt.lib" />
<LinkerArg Remove="/DEFAULTLIB:ucrt.lib" />
Expand Down
8 changes: 8 additions & 0 deletions dotnet/src/Devolutions.Pinget.Cli/Enums.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Devolutions.Pinget.Cli;

internal enum OutputFormat
{
Text,
Json,
Yaml,
}
18 changes: 18 additions & 0 deletions dotnet/src/Devolutions.Pinget.Cli/Extensions/CommandExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.CommandLine;

namespace Devolutions.Pinget.Cli.Extensions;

internal static partial class Extensions
{
extension(Command command)
{
public Command WithAliases(params string[] aliases)
{
foreach (var alias in aliases)
{
command.AddAlias(alias);
}
return command;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Devolutions.Pinget.Cli.Extensions;

internal static class ExceptionExtensions
{
extension(Exception ex)
{
internal bool CanIgnoreUnavailableImportFailure => ex is InvalidOperationException &&
(ex.Message.Contains("No package matched the query", StringComparison.OrdinalIgnoreCase) ||
ex.Message.Contains("No applicable installer found", StringComparison.OrdinalIgnoreCase));
}
}
18 changes: 18 additions & 0 deletions dotnet/src/Devolutions.Pinget.Cli/Extensions/OptionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.CommandLine;

namespace Devolutions.Pinget.Cli.Extensions;

internal static partial class Extensions
{
extension<T>(Option<T> option)
{
public Option<T> WithAliases(params string[] aliases)
{
foreach (var alias in aliases)
{
option.AddAlias(alias);
}
return option;
}
}
}
15 changes: 15 additions & 0 deletions dotnet/src/Devolutions.Pinget.Cli/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Devolutions.Pinget.Cli.Extensions;

internal static class StringExtensions
{
extension(string value)
{
public bool BooleanSetting =>
value.Trim().ToLowerInvariant() switch
{
"true" or "1" or "on" or "yes" or "enabled" => true,
"false" or "0" or "off" or "no" or "disabled" => false,
_ => throw new InvalidOperationException($"Unsupported admin setting value: {value}")
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Devolutions.Pinget.Core;

namespace Devolutions.Pinget.Cli.Helpers;

internal static class InstalledPackageChecker
{
internal static bool IsPresent(Repository repo, string packageId, string? sourceName) =>
repo.List(new ListQuery
{
Id = packageId,
Source = sourceName,
Exact = true,
Count = 1,
}).Matches.Count > 0;
}
29 changes: 29 additions & 0 deletions dotnet/src/Devolutions.Pinget.Cli/Helpers/Json.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using YamlDotNet.Serialization;

namespace Devolutions.Pinget.Cli.Helpers;

internal static class Json
{
internal static JsonSerializerOptions Options = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase };

internal static void WriteNode(JsonNode value, OutputFormat output)
{
switch (output)
{
case OutputFormat.Yaml:
var structured = JsonSerializer.Deserialize<object>(value.ToJsonString()) ?? new object();
Console.Write(new SerializerBuilder().Build().Serialize(structured));
break;
default:
Console.WriteLine(value.ToJsonString(new JsonSerializerOptions { WriteIndented = true }));
break;
}
}

internal static string? GetString(JsonElement element, string propertyName) =>
element.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String
? value.GetString()
: null;
}
50 changes: 50 additions & 0 deletions dotnet/src/Devolutions.Pinget.Cli/Helpers/Output.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Text.Json;
using Devolutions.Pinget.Core;
using YamlDotNet.Serialization;

namespace Devolutions.Pinget.Cli.Helpers;

internal static class Output
{
internal static OutputFormat GetFormat(string? value) =>
value?.ToLowerInvariant() switch
{
"json" => OutputFormat.Json,
"yaml" => OutputFormat.Yaml,
_ => OutputFormat.Text,
};

internal static void WriteStructuredOutput(object value, OutputFormat output)
{
switch (output)
{
case OutputFormat.Json:
if (value is SerializableShowManifest showManifest)
Console.WriteLine(JsonSerializer.Serialize(showManifest, PingetJsonContext.Default.SerializableShowManifest));
else
Console.WriteLine(JsonSerializer.Serialize(value, Json.Options));
break;
case OutputFormat.Yaml:
Console.Write(new SerializerBuilder().Build().Serialize(value));
break;
default:
throw new InvalidOperationException("Text output should be handled separately.");
}
}

internal static void WriteManifestStructuredOutput(object value, OutputFormat output)
{
if (output == OutputFormat.Yaml && value is List<Dictionary<string, object?>> documents)
{
var serializer = new SerializerBuilder().Build();
foreach (var document in documents)
{
Console.Write("---\n");
Console.Write(serializer.Serialize(document));
}
return;
}

WriteStructuredOutput(value, output);
}
}
66 changes: 66 additions & 0 deletions dotnet/src/Devolutions.Pinget.Cli/Helpers/Pin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Devolutions.Pinget.Core;

namespace Devolutions.Pinget.Cli.Helpers;

internal static class Pin
{
internal static List<PinRecord> Filter(Repository repo, PackageQuery query)
{
IEnumerable<PinRecord> pins = repo.ListPins(query.Source);
if (!string.IsNullOrWhiteSpace(query.Id))
pins = pins.Where(pin => Text.Matches(pin.PackageId, query.Id, query.Exact));

var needsCatalogResolution =
!string.IsNullOrWhiteSpace(query.Query) ||
!string.IsNullOrWhiteSpace(query.Name) ||
!string.IsNullOrWhiteSpace(query.Moniker) ||
!string.IsNullOrWhiteSpace(query.Tag) ||
!string.IsNullOrWhiteSpace(query.Command);
if (!needsCatalogResolution)
return pins.ToList();

var searchResult = repo.Search(query);
var keys = searchResult.Matches
.Select(match => $"{match.Id}|{match.SourceName ?? ""}")
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var ids = searchResult.Matches
.Select(match => match.Id)
.ToHashSet(StringComparer.OrdinalIgnoreCase);

return pins
.Where(pin => keys.Contains($"{pin.PackageId}|{pin.SourceId}") ||
(string.IsNullOrWhiteSpace(pin.SourceId) && ids.Contains(pin.PackageId)))
.ToList();
}

internal static PinRecord? FindMatching(ListMatch match, IReadOnlyList<PinRecord> pins)
{
PinRecord? sourceSpecific = null;
PinRecord? sourceAgnostic = null;
foreach (var pin in pins)
{
if (!pin.PackageId.Equals(match.Id, StringComparison.OrdinalIgnoreCase) &&
!pin.PackageId.Equals(match.LocalId, StringComparison.OrdinalIgnoreCase))
{
continue;
}

if (!string.IsNullOrWhiteSpace(pin.SourceId))
{
if (!string.IsNullOrWhiteSpace(match.SourceName) &&
pin.SourceId.Equals(match.SourceName, StringComparison.OrdinalIgnoreCase))
{
sourceSpecific = pin;
break;
}
}
else if (sourceAgnostic is null)
{
sourceAgnostic = pin;
}
}

return sourceSpecific ?? sourceAgnostic;
}

}
41 changes: 41 additions & 0 deletions dotnet/src/Devolutions.Pinget.Cli/Helpers/PinQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Devolutions.Pinget.Core;

namespace Devolutions.Pinget.Cli.Helpers;

internal static class PinQuery
{
internal static PackageQuery Create(
string? query,
string? id,
string? name,
string? moniker,
string? tag,
string? command,
string? source,
bool exact) =>
new()
{
Query = query,
Id = id,
Name = name,
Moniker = moniker,
Tag = tag,
Command = command,
Source = source,
Exact = exact,
Count = 200,
};

internal static void EnsureProvided(PackageQuery query, string commandName)
{
if (string.IsNullOrWhiteSpace(query.Query) &&
string.IsNullOrWhiteSpace(query.Id) &&
string.IsNullOrWhiteSpace(query.Name) &&
string.IsNullOrWhiteSpace(query.Moniker) &&
string.IsNullOrWhiteSpace(query.Tag) &&
string.IsNullOrWhiteSpace(query.Command))
{
throw new InvalidOperationException($"{commandName} requires a query or explicit filter.");
}
}
}
37 changes: 37 additions & 0 deletions dotnet/src/Devolutions.Pinget.Cli/Helpers/PinTarget.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Devolutions.Pinget.Core;

namespace Devolutions.Pinget.Cli.Helpers;

internal static class PinTarget
{
internal static SearchMatch ResolveSingleAvailable(Repository repo, PackageQuery query)
{
var result = repo.Search(query);
if (result.Matches.Count == 0)
throw new InvalidOperationException("No package matched the query.");
if (result.Matches.Count > 1)
throw new InvalidOperationException("Multiple packages matched the query; refine the query.");
return result.Matches[0];
}

internal static ListMatch ResolveSingleInstalled(Repository repo, PackageQuery query)
{
var result = repo.List(new ListQuery
{
Query = query.Query,
Id = query.Id,
Name = query.Name,
Moniker = query.Moniker,
Tag = query.Tag,
Command = query.Command,
Source = query.Source,
Exact = query.Exact,
Count = 200,
});
if (result.Matches.Count == 0)
throw new InvalidOperationException("No installed package matched the query.");
if (result.Matches.Count > 1)
throw new InvalidOperationException("Multiple installed packages matched the query; refine the query.");
return result.Matches[0];
}
}
Loading