Browse Source

Add Installed package extension managing

pull/438/head
ionite34 10 months ago
parent
commit
e27a00b814
No known key found for this signature in database
GPG Key ID: B3404C5F3827849B
  1. 11
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  2. 183
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageExtensionBrowserViewModel.cs
  3. 195
      StabilityMatrix.Avalonia/Views/PackageManager/PackageExtensionBrowserView.axaml
  4. 11
      StabilityMatrix.Avalonia/Views/PackageManager/PackageExtensionBrowserView.axaml.cs
  5. 29
      StabilityMatrix.Core/Models/GitVersion.cs
  6. 34
      StabilityMatrix.Core/Models/Packages/Extensions/InstalledPackageExtension.cs
  7. 5
      StabilityMatrix.Core/Models/Packages/Extensions/PackageExtensionVersion.cs

11
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -37,6 +37,7 @@ using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Packages.Extensions;
@ -487,6 +488,7 @@ public static class DesignData
DialogFactory.Get<PackageExtensionBrowserViewModel>(vm =>
{
vm.AddExtensions(
[
new PackageExtension
{
Author = "123",
@ -495,6 +497,15 @@ public static class DesignData
Reference = new Uri("https://github.com/LykosAI/StabilityMatrix"),
Files = [new Uri("https://github.com/LykosAI/StabilityMatrix")]
}
],
[
new InstalledPackageExtension
{
GitRepositoryUrl = "https://github.com/LykosAI/StabilityMatrix",
Paths = [new DirectoryPath("example-dir")]
},
new InstalledPackageExtension { Paths = [new DirectoryPath("example-dir-2")] }
]
);
});

183
StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageExtensionBrowserViewModel.cs

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
@ -9,6 +11,8 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
using DynamicData.Binding;
using StabilityMatrix.Avalonia.Collections;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.PackageManager;
using StabilityMatrix.Core.Attributes;
@ -23,15 +27,12 @@ namespace StabilityMatrix.Avalonia.ViewModels.PackageManager;
[View(typeof(PackageExtensionBrowserView))]
[Transient]
[ManagedService]
public partial class PackageExtensionBrowserViewModel : ViewModelBase
public partial class PackageExtensionBrowserViewModel : ViewModelBase, IDisposable
{
public PackagePair? PackagePair { get; set; }
[ObservableProperty]
private string searchFilter = string.Empty;
private readonly INotificationService notificationService;
private readonly IDisposable cleanUp;
[ObservableProperty]
private int selectedItemsCount;
public PackagePair? PackagePair { get; set; }
[ObservableProperty]
private bool isLoading;
@ -39,67 +40,94 @@ public partial class PackageExtensionBrowserViewModel : ViewModelBase
private SourceCache<PackageExtension, string> availableExtensionsSource =
new(ext => ext.Author + ext.Title + ext.Reference);
public IObservableCollection<SelectableItem<PackageExtension>> AvailableItems { get; } =
new ObservableCollectionExtended<SelectableItem<PackageExtension>>();
public IObservableCollection<SelectableItem<PackageExtension>> AvailableItemsFiltered { get; } =
new ObservableCollectionExtended<SelectableItem<PackageExtension>>();
public IObservableCollection<SelectableItem<PackageExtension>> SelectedAvailableItems { get; } =
new ObservableCollectionExtended<SelectableItem<PackageExtension>>();
public SearchCollection<
SelectableItem<PackageExtension>,
string,
string
> AvailableItemsSearchCollection { get; }
private SourceCache<InstalledPackageExtension, string> installedExtensionsSource =
new(
ext =>
ext.Paths.FirstOrDefault()?.ToString() ?? ext.GitRepositoryUrl ?? ext.GetHashCode().ToString()
);
public IObservableCollection<SelectableItem<InstalledPackageExtension>> SelectedInstalledItems { get; } =
new ObservableCollectionExtended<SelectableItem<InstalledPackageExtension>>();
public SearchCollection<
SelectableItem<InstalledPackageExtension>,
string,
string
> InstalledItemsSearchCollection { get; }
public IObservableCollection<InstalledPackageExtension> InstalledExtensions { get; } =
new ObservableCollectionExtended<InstalledPackageExtension>();
public PackageExtensionBrowserViewModel()
public PackageExtensionBrowserViewModel(INotificationService notificationService)
{
var searchPredicate = this.WhenPropertyChanged(vm => vm.SearchFilter)
.Select(
change =>
string.IsNullOrWhiteSpace(change.Value)
? _ => true
: new Func<SelectableItem<PackageExtension>, bool>(
ext => ext.Item.Title.Contains(change.Value, StringComparison.OrdinalIgnoreCase)
)
)
.AsObservable();
this.notificationService = notificationService;
var availableItemsChangeSet = availableExtensionsSource
.Connect()
.Transform(ext => new SelectableItem<PackageExtension>(ext))
.Publish();
availableItemsChangeSet
.SortBy(x => x.Item.Title)
.Bind(AvailableItems)
.Filter(searchPredicate)
.Bind(AvailableItemsFiltered)
.Subscribe();
availableItemsChangeSet
.AutoRefresh(item => item.IsSelected)
.Filter(item => item.IsSelected)
.ForEachChange(OnSelectedItemsUpdate)
.Bind(SelectedAvailableItems)
.Subscribe();
availableItemsChangeSet.Connect();
var installedItemsChangeSet = installedExtensionsSource
.Connect()
.Transform(ext => new SelectableItem<InstalledPackageExtension>(ext))
.Publish();
installedItemsChangeSet
.AutoRefresh(item => item.IsSelected)
.Filter(item => item.IsSelected)
.Bind(SelectedInstalledItems)
.Subscribe();
SelectedAvailableItems
.WhenPropertyChanged(x => x.Count)
.Select(x => x.Value)
.Subscribe(x => SelectedItemsCount = x);
cleanUp = new CompositeDisposable(
AvailableItemsSearchCollection = new SearchCollection<
SelectableItem<PackageExtension>,
string,
string
>(
availableItemsChangeSet,
query =>
string.IsNullOrWhiteSpace(query)
? _ => true
: x => x.Item.Title.Contains(query, StringComparison.OrdinalIgnoreCase)
),
availableItemsChangeSet.Connect(),
InstalledItemsSearchCollection = new SearchCollection<
SelectableItem<InstalledPackageExtension>,
string,
string
>(
installedItemsChangeSet,
query =>
string.IsNullOrWhiteSpace(query)
? _ => true
: x => x.Item.Title.Contains(query, StringComparison.OrdinalIgnoreCase)
),
installedItemsChangeSet.Connect()
);
}
public void AddExtensions(params PackageExtension[] packageExtensions)
public void AddExtensions(
IEnumerable<PackageExtension> packageExtensions,
IEnumerable<InstalledPackageExtension> installedExtensions
)
{
availableExtensionsSource.AddOrUpdate(packageExtensions);
installedExtensionsSource.AddOrUpdate(installedExtensions);
}
[RelayCommand]
@ -156,23 +184,34 @@ public partial class PackageExtensionBrowserViewModel : ViewModelBase
{
if (Design.IsDesignMode)
{
var (availableExtensions, installedExtensions) = SynchronizeExtensions(
availableExtensionsSource.Items,
installedExtensionsSource.Items
);
availableExtensionsSource.EditDiff(availableExtensions);
installedExtensionsSource.EditDiff(installedExtensions);
await Task.Delay(250);
}
else
{
// Refresh installed
var availableExtensions = await extensionManager.GetManifestExtensionsAsync(
extensionManager.GetManifests(PackagePair.InstalledPackage)
);
var installedExtensions = await extensionManager.GetInstalledExtensionsAsync(
PackagePair.InstalledPackage
);
installedExtensionsSource.EditDiff(installedExtensions);
// Refresh available
var extensions = await extensionManager.GetManifestExtensionsAsync(
extensionManager.GetManifests(PackagePair.InstalledPackage)
// Synchronize
(availableExtensions, installedExtensions) = SynchronizeExtensions(
availableExtensions,
installedExtensions
);
availableExtensionsSource.EditDiff(extensions);
availableExtensionsSource.EditDiff(availableExtensions);
installedExtensionsSource.EditDiff(installedExtensions);
}
}
finally
@ -187,10 +226,58 @@ public partial class PackageExtensionBrowserViewModel : ViewModelBase
{
item.IsSelected = false;
}
foreach (var item in SelectedInstalledItems.ToImmutableArray())
{
item.IsSelected = false;
}
}
[Pure]
private static (
IEnumerable<PackageExtension>,
IEnumerable<InstalledPackageExtension>
) SynchronizeExtensions(
IEnumerable<PackageExtension> extensions,
IEnumerable<InstalledPackageExtension> installedExtensions
)
{
using var _ = CodeTimer.StartDebug();
var extensionsArr = extensions.ToImmutableArray();
// For extensions, map their file paths for lookup
var repoToExtension = extensionsArr
.SelectMany(ext => ext.Files.Select(path => (path, ext)))
.ToLookup(kv => kv.path.ToString().StripEnd(".git"))
.ToDictionary(group => group.Key, x => x.First().ext);
// For installed extensions, add remote repo if available
var installedExtensionsWithDefinition = installedExtensions.Select(
installedExt =>
installedExt.GitRepositoryUrl is null
|| !repoToExtension.TryGetValue(
installedExt.GitRepositoryUrl.StripEnd(".git"),
out var mappedExt
)
? installedExt
: installedExt with
{
Definition = mappedExt
}
);
return (extensionsArr, installedExtensionsWithDefinition);
}
private void OnSelectedItemsUpdate(Change<SelectableItem<PackageExtension>, string> change)
/// <inheritdoc />
public void Dispose()
{
Debug.WriteLine($"{change}");
availableExtensionsSource.Dispose();
installedExtensionsSource.Dispose();
cleanUp.Dispose();
GC.SuppressFinalize(this);
}
}

195
StabilityMatrix.Avalonia/Views/PackageManager/PackageExtensionBrowserView.axaml

@ -14,6 +14,7 @@
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
d:DataContext="{x:Static mocks:DesignData.PackageExtensionBrowserViewModel}"
x:DataType="packageManager:PackageExtensionBrowserViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
@ -21,6 +22,7 @@
<controls:UserControlBase.Resources>
<!--<input:StandardUICommand x:Key="InstallExtensionCommand"
Command="{Binding InstallExtensionCommand}" />-->
<converters:FileUriConverter x:Key="FileUriConverter"/>
<DataTemplate x:Key="PackageExtensionTemplate">
<DataTemplate.DataType>
@ -68,17 +70,70 @@
Margin="0,0,16,0"
Grid.Column="2"/>
<!--<fluentIcons:SymbolIcon Grid.Column="2"
HorizontalAlignment="Right"
</sg:SpacedGrid>
</controls:Card>
</DataTemplate>
<DataTemplate x:Key="InstalledPackageExtensionTemplate">
<DataTemplate.DataType>
<x:Type TypeName="models:SelectableItem" x:TypeArguments="extensions:InstalledPackageExtension"/>
</DataTemplate.DataType>
<controls:Card
Padding="12,12"
Margin="4"
Classes="transparent-full"
CornerRadius="8">
<sg:SpacedGrid ColumnDefinitions="*,Auto">
<StackPanel
DataContext="{Binding Item}"
Orientation="Vertical" Spacing="4">
<TextBlock
Text="{Binding Title}"
FontWeight="Light"
FontSize="16"/>
<TextBlock Text="{Binding Version, FallbackValue=''}"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
FontSize="13" />
<TextBlock TextWrapping="Wrap"
Text="{Binding Definition.Description, FallbackValue=''}"
Foreground="{DynamicResource TextFillColorSecondary}"
FontWeight="Light"
FontSize="13" />
<StackPanel Orientation="Horizontal" Spacing="2">
<controls:HyperlinkIconButton
IsEnabled="{Binding Definition, Converter={x:Static ObjectConverters.IsNotNull}}"
Margin="0"
Padding="0,2"
NavigateUri="{Binding Definition.Reference, FallbackValue=''}"
Content="GitHub"/>
<Ellipse
Width="3"
Height="3"
Opacity="0.8"
VerticalAlignment="Center"
FontSize="24"
Symbol="ChevronRight" />-->
Fill="{DynamicResource TextFillColorTertiaryBrush}" />
<controls:HyperlinkIconButton
IsEnabled="{Binding Definition, Converter={x:Static ObjectConverters.IsNotNull}}"
Margin="0"
Padding="0,2"
NavigateUri="{Binding PrimaryPath.FullPath, Converter={StaticResource FileUriConverter}}"
Content="{Binding DisplayPath}"/>
</StackPanel>
</StackPanel>
<CheckBox
Grid.Column="1"
HorizontalAlignment="Right"
IsChecked="{Binding IsSelected}"
Margin="0,0,16,0"/>
</sg:SpacedGrid>
</controls:Card>
</DataTemplate>
</controls:UserControlBase.Resources>
<TabControl TabStripPlacement="Top">
<TabControl TabStripPlacement="Top" SelectionChanged="TabControl_OnSelectionChanged">
<TabControl.Items>
<TabItem FontSize="18" Header="Available Extensions" Margin="8,0,0,0">
<sg:SpacedGrid
@ -89,19 +144,20 @@
ColumnDefinitions="*,Auto,Auto"
ColumnSpacing="8">
<TextBox Margin="4, 0,0,0"
DataContext="{Binding AvailableItemsSearchCollection}"
Watermark="{x:Static lang:Resources.Action_Search}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding SearchFilter, Mode=TwoWay}">
Text="{Binding Query, Mode=TwoWay}">
<TextBox.InnerRightContent>
<Grid>
<Button Classes="transparent-full"
IsVisible="{Binding SearchFilter.Length}"
Command="{Binding $parent[TextBox].Clear}">
IsVisible="{Binding Query.Length}"
Command="{Binding ClearQuery}">
<ui:SymbolIcon Symbol="Cancel" />
</Button>
<ui:SymbolIcon
IsVisible="{Binding !SearchFilter.Length}"
IsVisible="{Binding !Query.Length}"
Margin="0,0,10,0"
FontSize="16"
Symbol="Find" />
@ -133,19 +189,31 @@
<ScrollViewer Grid.Row="1">
<ItemsRepeater
DataContext="{Binding AvailableItemsSearchCollection}"
ItemTemplate="{StaticResource PackageExtensionTemplate}"
ItemsSource="{Binding AvailableItemsFiltered}">
ItemsSource="{Binding FilteredItems}">
<ItemsRepeater.Layout>
<StackLayout Orientation="Vertical" />
</ItemsRepeater.Layout>
</ItemsRepeater>
</ScrollViewer>
<Panel
IsVisible="{Binding IsLoading}"
Grid.Row="1">
<ProgressBar
Margin="32,16"
IsIndeterminate="True"
IsEnabled="{Binding IsLoading}"
IsVisible="{Binding IsLoading}"/>
</Panel>
<ui:InfoBar
Grid.Row="2"
IsOpen="{Binding !!SelectedAvailableItems.Count}"
HorizontalContentAlignment="Right"
HorizontalAlignment="Right"
CloseButtonCommand="{Binding ClearSelection}"
IsIconVisible="False">
<ui:InfoBar.Title>
<MultiBinding StringFormat="{}{0} Selected">
@ -163,53 +231,108 @@
</sg:SpacedGrid>
</TabItem>
<TabItem FontSize="18" Header="Installed Extensions" Margin="0,0,0,0"/>
<TabItem FontSize="18" Header="Installed Extensions" Margin="0,0,0,0">
<!--<TabItem Header="Training">
<ScrollViewer Padding="8">
<StackPanel Orientation="Vertical">
<Grid ColumnDefinitions="*, Auto" Margin="0,0,0,8">
<sg:SpacedGrid
RowDefinitions="Auto,*,Auto"
RowSpacing="12"
Margin="8">
<sg:SpacedGrid
ColumnDefinitions="*,Auto,Auto"
ColumnSpacing="8">
<TextBox Margin="4, 0,0,0"
DataContext="{Binding InstalledItemsSearchCollection}"
Watermark="{x:Static lang:Resources.Action_Search}"
Height="36"
FontSize="16"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
KeyDown="InputElement_OnKeyDown"
Text="{Binding SearchFilter, Mode=TwoWay}">
Text="{Binding Query, Mode=TwoWay}">
<TextBox.InnerRightContent>
<Grid>
<Button Classes="transparent-full"
IsVisible="{Binding SearchFilter.Length}"
Command="{Binding ClearSearchQuery}">
<controls1:SymbolIcon Symbol="Cancel" />
IsVisible="{Binding Query.Length}"
Command="{Binding ClearQuery}">
<ui:SymbolIcon Symbol="Cancel" />
</Button>
<controls1:SymbolIcon
IsVisible="{Binding !SearchFilter.Length}"
<ui:SymbolIcon
IsVisible="{Binding !Query.Length}"
Margin="0,0,10,0"
FontSize="16"
Symbol="Find" />
</Grid>
</TextBox.InnerRightContent>
</TextBox>
<ToggleSwitch Grid.Column="1" OnContent="Show All Packages" OffContent="Show All Packages"
IsChecked="{Binding ShowIncompatiblePackages}"
VerticalAlignment="Center"
<!-- Refresh Button -->
<Button
Grid.Row="0"
Grid.Column="1"
Padding="16,6"
HorizontalAlignment="Left"
Margin="16, 0, 8, 0"
ToolTip.Tip="Enabling &quot;Show All Packages&quot; will include packages that may not be compatible with your system and may run very slowly as a result." />
</Grid>
<ItemsRepeater ItemsSource="{Binding InstalledExtensions}">
VerticalAlignment="Stretch"
Command="{Binding RefreshCommand}">
<ui:SymbolIcon FontSize="16" Symbol="Refresh" />
</Button>
<!-- Settings / Edit Manifests Button -->
<Button
Grid.Row="0"
Grid.Column="2"
Padding="16,6"
HorizontalAlignment="Left"
VerticalAlignment="Stretch">
<ui:SymbolIcon FontSize="16" Symbol="Settings" />
</Button>
</sg:SpacedGrid>
<ScrollViewer Grid.Row="1">
<ItemsRepeater
DataContext="{Binding InstalledItemsSearchCollection}"
ItemTemplate="{StaticResource InstalledPackageExtensionTemplate}"
ItemsSource="{Binding FilteredItems}">
<ItemsRepeater.Layout>
<StackLayout Orientation="Vertical" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<StaticResource ResourceKey="PackageExtensionTemplate" />
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</StackPanel>
</ScrollViewer>
</TabItem>-->
<Panel
IsVisible="{Binding IsLoading}"
Grid.Row="1"
>
<ProgressBar
Margin="32,16"
IsIndeterminate="True"
IsEnabled="{Binding IsLoading}"
IsVisible="{Binding IsLoading}"/>
</Panel>
<ui:InfoBar
Grid.Row="2"
IsOpen="{Binding !!SelectedInstalledItems.Count}"
HorizontalContentAlignment="Right"
HorizontalAlignment="Right"
CloseButtonCommand="{Binding ClearSelection}"
IsIconVisible="False">
<ui:InfoBar.Title>
<MultiBinding StringFormat="{}{0} Selected">
<Binding Path="SelectedInstalledItems.Count"/>
</MultiBinding>
</ui:InfoBar.Title>
<ui:InfoBar.ActionButton>
<StackPanel Orientation="Horizontal" Spacing="6">
<Button
Classes="accent"
Content="{x:Static lang:Resources.Action_Update}"/>
<Button
Content="{x:Static lang:Resources.Action_Uninstall}"/>
</StackPanel>
</ui:InfoBar.ActionButton>
</ui:InfoBar>
</sg:SpacedGrid>
</TabItem>
</TabControl.Items>
</TabControl>
</controls:UserControlBase>

11
StabilityMatrix.Avalonia/Views/PackageManager/PackageExtensionBrowserView.axaml.cs

@ -1,4 +1,6 @@
using Avalonia.Controls;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views.PackageManager;
@ -10,4 +12,13 @@ public partial class PackageExtensionBrowserView : UserControlBase
{
InitializeComponent();
}
private void TabControl_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
// Clear selection when switching tabs
if (DataContext is PackageExtensionBrowserViewModel vm)
{
vm.ClearSelection();
}
}
}

29
StabilityMatrix.Core/Models/GitVersion.cs

@ -3,11 +3,38 @@ namespace StabilityMatrix.Core.Models;
/// <summary>
/// Union of either Tag or Branch + CommitSha.
/// </summary>
public record GitVersion
public record GitVersion : IFormattable
{
public string? Tag { get; init; }
public string? Branch { get; init; }
public string? CommitSha { get; init; }
/// <inheritdoc />
public override string ToString()
{
if (!string.IsNullOrEmpty(Tag))
{
return Tag;
}
if (!string.IsNullOrEmpty(Branch) && !string.IsNullOrEmpty(CommitSha))
{
return $"{Branch}@{CommitSha[..7]}";
}
if (!string.IsNullOrEmpty(Branch))
{
return Branch;
}
return !string.IsNullOrEmpty(CommitSha) ? CommitSha[..7] : "";
}
/// <inheritdoc />
public string ToString(string? format, IFormatProvider? formatProvider)
{
return ToString();
}
}

34
StabilityMatrix.Core/Models/Packages/Extensions/InstalledPackageExtension.cs

@ -9,6 +9,11 @@ public record InstalledPackageExtension
/// </summary>
public required IEnumerable<IPathObject> Paths { get; init; }
/// <summary>
/// Primary path of the extension.
/// </summary>
public IPathObject? PrimaryPath => Paths.FirstOrDefault();
/// <summary>
/// The version of the extension.
/// </summary>
@ -23,4 +28,33 @@ public record InstalledPackageExtension
/// The PackageExtension definition, if available.
/// </summary>
public PackageExtension? Definition { get; init; }
public string Title
{
get
{
if (Definition?.Title is { } title)
{
return title;
}
if (Paths.FirstOrDefault()?.Name is { } pathName)
{
return pathName;
}
return "";
}
}
/// <summary>
/// Path containing PrimaryPath and its parent.
/// </summary>
public string DisplayPath =>
PrimaryPath switch
{
null => "",
DirectoryPath { Parent: { } parentDir } dir => $"{parentDir.Name}/{dir.Name}",
_ => PrimaryPath.Name
};
}

5
StabilityMatrix.Core/Models/Packages/Extensions/PackageExtensionVersion.cs

@ -1,3 +1,6 @@
namespace StabilityMatrix.Core.Models.Packages.Extensions;
public record PackageExtensionVersion : GitVersion;
public record PackageExtensionVersion : GitVersion
{
public override string ToString() => base.ToString();
};

Loading…
Cancel
Save