diff --git a/README.md b/README.md index 47e686c2..05601667 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ RepoM is a minimal-conf git repository hub with Windows Explorer enhancements. I It's populating itself as you work with git. It does not get in the way and does not require any user attention to work. -Repo< will not compete with your favourite git clients, so keep them. It's not about working within a repository: It's a new way to use all of your repositories to make your daily work easier. +RepoM will not compete with your favourite git clients, so keep them. It's not about working within a repository: It's a new way to use all of your repositories to make your daily work easier. 📦 [Check the Releases page](https://github.com/coenm/RepoM/releases) to **download** the latest version and see **what's new**! @@ -26,7 +26,7 @@ For Windows, use the hotkeys Ctrl+Alt+R to show To open a file browser, simply press Return on the keyboard once you selected a repository. To open a command prompt instead, hold Ctrl on Windows or Command on macOS while pressing Return. These modifier keys will also work with mouse navigation. -## Enhanced Windows Explorer Titles +## Plugin: Enhanced Windows Explorer Titles As an extra goodie for Windows users, RepoZ automatically detects open File Explorer windows and adds a status appendix to their title if they are in context of a git repository. @@ -34,4 +34,4 @@ As an extra goodie for Windows users, RepoZ automatically detects open File Expl ## Credits -RepoM is a fork of the amazing RepoZ, which was created by [Screenshot](https://github.com/awaescher/RepoZ). +RepoM is a fork of the amazing RepoZ, which was created by [Andreas Wäscher](https://github.com/awaescher/RepoZ). diff --git a/RepoM.sln b/RepoM.sln index 5802ec6e..7bbbd73a 100644 --- a/RepoM.sln +++ b/RepoM.sln @@ -33,6 +33,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RepoM.Api.Tests", "tests\Re EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RepoM.Core.Plugin", "src\RepoM.Core.Plugin\RepoM.Core.Plugin.csproj", "{E38D9928-A0A6-4978-BD7E-C7F2E2B6BC9B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RepoM.Plugin.Statistics", "src\RepoM.Plugin.Statistics\RepoM.Plugin.Statistics.csproj", "{DF4F5FA8-0E3F-4D84-A048-20242FD032BC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RepoM.Plugin.Statistics.Tests", "tests\RepoM.Plugin.Statistics.Tests\RepoM.Plugin.Statistics.Tests.csproj", "{C90AA844-B355-4C62-96FA-D4F338EE094A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -327,6 +331,46 @@ Global {E38D9928-A0A6-4978-BD7E-C7F2E2B6BC9B}.Release|x64.Build.0 = Release|Any CPU {E38D9928-A0A6-4978-BD7E-C7F2E2B6BC9B}.Release|x86.ActiveCfg = Release|Any CPU {E38D9928-A0A6-4978-BD7E-C7F2E2B6BC9B}.Release|x86.Build.0 = Release|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Debug|ARM.ActiveCfg = Debug|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Debug|ARM.Build.0 = Debug|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Debug|ARM64.Build.0 = Debug|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Debug|x64.ActiveCfg = Debug|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Debug|x64.Build.0 = Debug|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Debug|x86.ActiveCfg = Debug|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Debug|x86.Build.0 = Debug|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Release|Any CPU.Build.0 = Release|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Release|ARM.ActiveCfg = Release|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Release|ARM.Build.0 = Release|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Release|ARM64.ActiveCfg = Release|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Release|ARM64.Build.0 = Release|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Release|x64.ActiveCfg = Release|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Release|x64.Build.0 = Release|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Release|x86.ActiveCfg = Release|Any CPU + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC}.Release|x86.Build.0 = Release|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Debug|ARM.ActiveCfg = Debug|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Debug|ARM.Build.0 = Debug|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Debug|ARM64.Build.0 = Debug|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Debug|x64.ActiveCfg = Debug|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Debug|x64.Build.0 = Debug|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Debug|x86.ActiveCfg = Debug|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Debug|x86.Build.0 = Debug|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Release|Any CPU.Build.0 = Release|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Release|ARM.ActiveCfg = Release|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Release|ARM.Build.0 = Release|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Release|ARM64.ActiveCfg = Release|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Release|ARM64.Build.0 = Release|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Release|x64.ActiveCfg = Release|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Release|x64.Build.0 = Release|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Release|x86.ActiveCfg = Release|Any CPU + {C90AA844-B355-4C62-96FA-D4F338EE094A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -340,6 +384,8 @@ Global {74B95BAF-0DB4-42C8-92EA-A364E8080809} = {D6E372DC-10D3-4997-9DFC-568B4666635A} {26FF9590-66C7-4B87-9574-E816E1E36628} = {D6E372DC-10D3-4997-9DFC-568B4666635A} {C2014EE2-F17C-478E-B282-9D0971065BB8} = {D6E372DC-10D3-4997-9DFC-568B4666635A} + {DF4F5FA8-0E3F-4D84-A048-20242FD032BC} = {D6E372DC-10D3-4997-9DFC-568B4666635A} + {C90AA844-B355-4C62-96FA-D4F338EE094A} = {D6E372DC-10D3-4997-9DFC-568B4666635A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1765ABAA-0652-4DA5-ABBF-05396F2957D7} diff --git a/RepoM.sln.DotSettings b/RepoM.sln.DotSettings index 7f128dd7..abeb3a9d 100644 --- a/RepoM.sln.DotSettings +++ b/RepoM.sln.DotSettings @@ -1,5 +1,7 @@  True + True + True True True True diff --git a/src/RepoM.Api/Common/AppSettings.cs b/src/RepoM.Api/Common/AppSettings.cs index eb2d8b76..a152c130 100644 --- a/src/RepoM.Api/Common/AppSettings.cs +++ b/src/RepoM.Api/Common/AppSettings.cs @@ -11,7 +11,9 @@ public AppSettings() EnabledSearchProviders = new List(); SonarCloudPersonalAccessToken = string.Empty; AzureDevOps = AzureDevOpsOptions.Default; + SortKey = string.Empty; } + public string SortKey { get; set; } public AutoFetchMode AutoFetchMode { get; set; } diff --git a/src/RepoM.Api/Common/FileAppSettingsService.cs b/src/RepoM.Api/Common/FileAppSettingsService.cs index 0c92a184..b172d510 100644 --- a/src/RepoM.Api/Common/FileAppSettingsService.cs +++ b/src/RepoM.Api/Common/FileAppSettingsService.cs @@ -7,7 +7,67 @@ namespace RepoM.Api.Common; using System.Linq; using Newtonsoft.Json; using RepoM.Api.Git.AutoFetch; -using RepoM.Api.IO; +using RepoM.Core.Plugin.Common; +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +public class FilesICompareSettingsService : ICompareSettingsService +{ + private readonly IFileSystem _fileSystem; + private readonly IEnumerable _registrations; + private readonly IAppDataPathProvider _appDataPathProvider; + private Dictionary? _configuration; + + + public FilesICompareSettingsService(IAppDataPathProvider appDataPathProvider, IFileSystem fileSystem, IEnumerable registrations) + { + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _registrations = registrations.ToList(); + _appDataPathProvider = appDataPathProvider ?? throw new ArgumentNullException(nameof(appDataPathProvider)); + } + + public Dictionary Configuration => _configuration ??= Load(); + + + private string GetFileName() + { + return _fileSystem.Path.Combine(_appDataPathProvider.GetAppDataPath(), "RepoM.Ordering.yaml"); + } + + private Dictionary Load() + { + var file = GetFileName(); + + if (!_fileSystem.File.Exists(file)) + { + throw new Exception("File doesn't exist"); + } + + try + { + var yml = _fileSystem.File.ReadAllText(file); + + DeserializerBuilder builder = new DeserializerBuilder() + .WithNamingConvention(HyphenatedNamingConvention.Instance); + + foreach (IConfigurationRegistration instance in _registrations) + { + var tag = instance.Tag.TrimStart('!'); + builder.WithTagMapping("!" + tag, instance.ConfigurationType); + } + + IDeserializer deserializer = builder.Build(); + + return deserializer.Deserialize>(yml); + } + catch + { + throw; + /* Our app settings are not critical. For our purposes, we want to ignore IO exceptions */ + } + } +} public class FileAppSettingsService : IAppSettingsService { @@ -72,6 +132,24 @@ private string GetFileName() private AppSettings Settings => _settings ??= Load(); + + public string SortKey + { + get => Settings.SortKey; + set + { + if (value == Settings.SortKey) + { + return; + } + + Settings.SortKey = value; + + NotifyChange(); + Save(); + } + } + public AutoFetchMode AutoFetchMode { get => Settings.AutoFetchMode; diff --git a/src/RepoM.Api/Common/HardcodededMiniHumanizer.cs b/src/RepoM.Api/Common/HardcodededMiniHumanizer.cs index ac358df1..4cb163b7 100644 --- a/src/RepoM.Api/Common/HardcodededMiniHumanizer.cs +++ b/src/RepoM.Api/Common/HardcodededMiniHumanizer.cs @@ -1,16 +1,12 @@ namespace RepoM.Api.Common; using System; +using RepoM.Core.Plugin.Common; public class HardcodededMiniHumanizer : IHumanizer { private readonly IClock _clock; - public HardcodededMiniHumanizer() - : this(new SystemClock()) - { - } - public HardcodededMiniHumanizer(IClock clock) { _clock = clock ?? throw new ArgumentNullException(nameof(clock)); diff --git a/src/RepoM.Api/Common/IAppSettingsService.cs b/src/RepoM.Api/Common/IAppSettingsService.cs index 043880e5..e49c1b0e 100644 --- a/src/RepoM.Api/Common/IAppSettingsService.cs +++ b/src/RepoM.Api/Common/IAppSettingsService.cs @@ -3,6 +3,12 @@ namespace RepoM.Api.Common; using System; using System.Collections.Generic; using RepoM.Api.Git.AutoFetch; +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +public interface ICompareSettingsService +{ + Dictionary Configuration { get; } +} public interface IAppSettingsService { @@ -22,5 +28,7 @@ public interface IAppSettingsService string AzureDevOpsBaseUrl { get; set; } + string SortKey { get; set; } + void RegisterInvalidationHandler(Action handler); } diff --git a/src/RepoM.Api/Common/SystemClock.cs b/src/RepoM.Api/Common/SystemClock.cs index 7c403999..255697d1 100644 --- a/src/RepoM.Api/Common/SystemClock.cs +++ b/src/RepoM.Api/Common/SystemClock.cs @@ -1,8 +1,15 @@ namespace RepoM.Api.Common; using System; +using RepoM.Core.Plugin.Common; public class SystemClock : IClock { + private SystemClock() + { + } + + public static SystemClock Instance { get; } = new(); + public DateTime Now => DateTime.Now; } \ No newline at end of file diff --git a/src/RepoM.Api/Git/AutoFetch/DefaultAutoFetchHandler.cs b/src/RepoM.Api/Git/AutoFetch/DefaultAutoFetchHandler.cs index 799bc92d..44b0f6f3 100644 --- a/src/RepoM.Api/Git/AutoFetch/DefaultAutoFetchHandler.cs +++ b/src/RepoM.Api/Git/AutoFetch/DefaultAutoFetchHandler.cs @@ -9,7 +9,7 @@ namespace RepoM.Api.Git.AutoFetch; public class DefaultAutoFetchHandler : IAutoFetchHandler { private bool _active; - private AutoFetchMode? _mode = null; + private AutoFetchMode? _mode; private readonly Timer _timer; private readonly Dictionary _profiles; private int _lastFetchRepository = -1; @@ -26,10 +26,10 @@ public DefaultAutoFetchHandler( _profiles = new Dictionary { - { AutoFetchMode.Off, new AutoFetchProfile() { PauseBetweenFetches = TimeSpan.MaxValue, } }, - { AutoFetchMode.Discretely, new AutoFetchProfile() { PauseBetweenFetches = TimeSpan.FromMinutes(5), } }, - { AutoFetchMode.Adequate, new AutoFetchProfile() { PauseBetweenFetches = TimeSpan.FromMinutes(1), } }, - { AutoFetchMode.Aggressive, new AutoFetchProfile() { PauseBetweenFetches = TimeSpan.FromSeconds(2), } }, + { AutoFetchMode.Off, new AutoFetchProfile { PauseBetweenFetches = TimeSpan.MaxValue, } }, + { AutoFetchMode.Discretely, new AutoFetchProfile { PauseBetweenFetches = TimeSpan.FromMinutes(5), } }, + { AutoFetchMode.Adequate, new AutoFetchProfile { PauseBetweenFetches = TimeSpan.FromMinutes(1), } }, + { AutoFetchMode.Aggressive, new AutoFetchProfile { PauseBetweenFetches = TimeSpan.FromSeconds(2), } }, }; _timer = new Timer(FetchNext, null, Timeout.Infinite, Timeout.Infinite); @@ -71,28 +71,28 @@ private void FetchNext(object timerState) // 2. makes sure that no repository is jumped over because the list // of repositories is constantly changed and not sorted in any way in memory. // So we cannot guarantuee that each repository is fetched on each iteration if we do not sort. - var repositories = RepositoryInformationAggregator.Repositories + var repositories = RepositoryInformationAggregator.Repositories? .OrderBy(r => r.Name) - .ToList(); + .ToArray() ?? Array.Empty(); // temporarily disable the timer to prevent parallel fetch executions UpdateBehavior(AutoFetchMode.Off); _lastFetchRepository++; - if (repositories.Count <= _lastFetchRepository) + if (repositories.Length <= _lastFetchRepository) { _lastFetchRepository = 0; } - RepositoryView repositoryView = repositories[_lastFetchRepository]; + RepositoryViewModel repositoryViewModel = repositories[_lastFetchRepository]; - Console.WriteLine($"Auto-fetching {repositoryView.Name} (index {_lastFetchRepository} of {repositories.Count})"); + Console.WriteLine($"Auto-fetching {repositoryViewModel.Name} (index {_lastFetchRepository} of {repositories.Length})"); - repositoryView.IsSynchronizing = true; + repositoryViewModel.IsSynchronizing = true; try { - RepositoryWriter.Fetch(repositoryView.Repository); + RepositoryWriter.Fetch(repositoryViewModel.Repository); } catch { @@ -103,7 +103,7 @@ private void FetchNext(object timerState) // re-enable the timer to get to the next fetch UpdateBehavior(); - repositoryView.IsSynchronizing = false; + repositoryViewModel.IsSynchronizing = false; } } @@ -134,7 +134,7 @@ public AutoFetchMode Mode } _mode = value; - Console.WriteLine("Auto fetch is: " + _mode.GetValueOrDefault().ToString()); + Console.WriteLine("Auto fetch is: " + _mode.GetValueOrDefault()); UpdateBehavior(); } diff --git a/src/RepoM.Api/Git/DefaultRepositoryIgnoreStore.cs b/src/RepoM.Api/Git/DefaultRepositoryIgnoreStore.cs index 303659cb..13fbda7f 100644 --- a/src/RepoM.Api/Git/DefaultRepositoryIgnoreStore.cs +++ b/src/RepoM.Api/Git/DefaultRepositoryIgnoreStore.cs @@ -5,7 +5,7 @@ namespace RepoM.Api.Git; using System.IO; using System.IO.Abstractions; using System.Linq; -using RepoM.Api.IO; +using RepoM.Core.Plugin.Common; public class DefaultRepositoryIgnoreStore : FileRepositoryStore, IRepositoryIgnoreStore { diff --git a/src/RepoM.Api/Git/DefaultRepositoryInformationAggregator.cs b/src/RepoM.Api/Git/DefaultRepositoryInformationAggregator.cs index 55faa4b1..eaf497ec 100644 --- a/src/RepoM.Api/Git/DefaultRepositoryInformationAggregator.cs +++ b/src/RepoM.Api/Git/DefaultRepositoryInformationAggregator.cs @@ -13,16 +13,16 @@ public class DefaultRepositoryInformationAggregator : IRepositoryInformationAggr public DefaultRepositoryInformationAggregator(IThreadDispatcher dispatcher) { _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); - Repositories = new ObservableCollection(); + Repositories = new ObservableCollection(); } - public ObservableCollection Repositories { get; } + public ObservableCollection Repositories { get; } public void Add(Repository repository, IRepositoryMonitor repositoryMonitor) { _dispatcher.Invoke(() => { - var view = new RepositoryView(repository, repositoryMonitor); + var view = new RepositoryViewModel(repository, repositoryMonitor); Repositories.Remove(view); Repositories.Add(view); @@ -33,7 +33,7 @@ public void RemoveByPath(string path) { _dispatcher.Invoke(() => { - RepositoryView[] viewsToRemove = Repositories.Where(r => r.Path.Equals(path, StringComparison.OrdinalIgnoreCase)).ToArray(); + RepositoryViewModel[] viewsToRemove = Repositories.Where(r => r.Path.Equals(path, StringComparison.OrdinalIgnoreCase)).ToArray(); for (var i = viewsToRemove.Length - 1; i >= 0; i--) { @@ -44,18 +44,18 @@ public void RemoveByPath(string path) public string? GetStatusByPath(string path) { - RepositoryView? view = GetRepositoryByPath(path); + RepositoryViewModel? view = GetRepositoryByPath(path); return view?.BranchWithStatus; } - private RepositoryView? GetRepositoryByPath(string path) + private RepositoryViewModel? GetRepositoryByPath(string path) { if (string.IsNullOrEmpty(path)) { return null; } - List? views = null; + List? views = null; try { views = Repositories.ToList(); @@ -76,7 +76,7 @@ public void RemoveByPath(string path) path += "\\"; } - RepositoryView[] viewsByPath = views! + RepositoryViewModel[] viewsByPath = views! .Where(r => r?.Path != null && diff --git a/src/RepoM.Api/Git/DefaultRepositoryReader.cs b/src/RepoM.Api/Git/DefaultRepositoryReader.cs index deba0a83..e42949a5 100644 --- a/src/RepoM.Api/Git/DefaultRepositoryReader.cs +++ b/src/RepoM.Api/Git/DefaultRepositoryReader.cs @@ -113,9 +113,9 @@ public DefaultRepositoryReader(IRepositoryTagsFactory resolver, ILogger logger) RemoteCollection? remoteCollection = repo.Network?.Remotes; if (remoteCollection != null) { - foreach (LibGit2Sharp.Remote r in remoteCollection.Where(r => !string.IsNullOrWhiteSpace(r.Name) && !string.IsNullOrWhiteSpace(r.Url))) + foreach (Remote r in remoteCollection.Where(r => !string.IsNullOrWhiteSpace(r.Name) && !string.IsNullOrWhiteSpace(r.Url))) { - repository.Remotes.Add(new Remote(r.Name.Trim(), r.Url.Trim())); + repository.Remotes.Add(new Core.Plugin.Repository.Remote(r.Name.Trim(), r.Url.Trim())); } } diff --git a/src/RepoM.Api/Git/DefaultRepositoryStore.cs b/src/RepoM.Api/Git/DefaultRepositoryStore.cs index 3ae39a8f..e6364569 100644 --- a/src/RepoM.Api/Git/DefaultRepositoryStore.cs +++ b/src/RepoM.Api/Git/DefaultRepositoryStore.cs @@ -3,7 +3,7 @@ namespace RepoM.Api.Git; using System; using System.IO; using System.IO.Abstractions; -using RepoM.Api.IO; +using RepoM.Core.Plugin.Common; public class DefaultRepositoryStore : FileRepositoryStore { diff --git a/src/RepoM.Api/Git/IRepositoryInformationAggregator.cs b/src/RepoM.Api/Git/IRepositoryInformationAggregator.cs index 4d159bbc..05c0ea8b 100644 --- a/src/RepoM.Api/Git/IRepositoryInformationAggregator.cs +++ b/src/RepoM.Api/Git/IRepositoryInformationAggregator.cs @@ -10,7 +10,7 @@ public interface IRepositoryInformationAggregator string? GetStatusByPath(string path); - ObservableCollection Repositories { get; } + ObservableCollection Repositories { get; } void Reset(); diff --git a/src/RepoM.Api/Git/IRepositoryView.cs b/src/RepoM.Api/Git/IRepositoryView.cs index e5291cea..2a2fa629 100644 --- a/src/RepoM.Api/Git/IRepositoryView.cs +++ b/src/RepoM.Api/Git/IRepositoryView.cs @@ -11,4 +11,6 @@ public interface IRepositoryView bool IsPinned { get; } bool HasUnpushedChanges { get; } + + Repository Repository { get; } } \ No newline at end of file diff --git a/src/RepoM.Api/Git/Repository.cs b/src/RepoM.Api/Git/Repository.cs index bd7a0f38..7ba85cc2 100644 --- a/src/RepoM.Api/Git/Repository.cs +++ b/src/RepoM.Api/Git/Repository.cs @@ -3,9 +3,10 @@ namespace RepoM.Api.Git; using System; using System.Collections.Generic; using System.Diagnostics; +using RepoM.Core.Plugin.Repository; [DebuggerDisplay("{Name} @{Path}")] -public class Repository +public class Repository : IRepository { public Repository() { @@ -17,7 +18,7 @@ public Repository() Location = string.Empty; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (obj is not Repository other) { diff --git a/src/RepoM.Api/Git/RepositoryAction.cs b/src/RepoM.Api/Git/RepositoryAction.cs index edf5e451..16c47e37 100644 --- a/src/RepoM.Api/Git/RepositoryAction.cs +++ b/src/RepoM.Api/Git/RepositoryAction.cs @@ -2,14 +2,23 @@ namespace RepoM.Api.Git; using System; using System.Collections.Generic; +using RepoM.Core.Plugin.Repository; +using RepoM.Core.Plugin.RepositoryActions; +using RepoM.Core.Plugin.RepositoryActions.Actions; public class RepositorySeparatorAction : RepositoryActionBase { + public RepositorySeparatorAction(IRepository repository) + : base(repository) + { + } } + public class RepositoryAction : RepositoryActionBase { - public RepositoryAction(string name) + public RepositoryAction(string name, IRepository repository): + base(repository) { Name = name; } @@ -19,7 +28,14 @@ public RepositoryAction(string name) public abstract class RepositoryActionBase { - public Action? Action { get; set; } + public RepositoryActionBase(IRepository repository) + { + Repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + + public IAction Action { get; set; } = NullAction.Instance; + + public IRepository Repository { get; } public bool ExecutionCausesSynchronizing { get; set; } diff --git a/src/RepoM.Api/Git/RepositoryView.cs b/src/RepoM.Api/Git/RepositoryViewModel.cs similarity index 95% rename from src/RepoM.Api/Git/RepositoryView.cs rename to src/RepoM.Api/Git/RepositoryViewModel.cs index e75284e6..1e315375 100644 --- a/src/RepoM.Api/Git/RepositoryView.cs +++ b/src/RepoM.Api/Git/RepositoryViewModel.cs @@ -6,7 +6,7 @@ namespace RepoM.Api.Git; using System.Linq; [DebuggerDisplay("{Name} @{Path}")] -public class RepositoryView : IRepositoryView, INotifyPropertyChanged +public class RepositoryViewModel : IRepositoryView, INotifyPropertyChanged { private readonly IRepositoryMonitor _monitor; private string? _cachedRepositoryStatusCode; @@ -16,7 +16,7 @@ public class RepositoryView : IRepositoryView, INotifyPropertyChanged public event PropertyChangedEventHandler? PropertyChanged; - public RepositoryView(Repository repository, IRepositoryMonitor monitor) + public RepositoryViewModel(Repository repository, IRepositoryMonitor monitor) { _monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); Repository = repository ?? throw new ArgumentNullException(nameof(repository)); @@ -25,7 +25,7 @@ public RepositoryView(Repository repository, IRepositoryMonitor monitor) public override bool Equals(object obj) { - if (obj is RepositoryView other) + if (obj is RepositoryViewModel other) { return other.Repository.Equals(Repository); } @@ -128,4 +128,5 @@ public bool IsSynchronizing private string SyncAppendix => " \u2191\u2193"; // up and down arrows public DateTime UpdateStampUtc { get; private set; } + } \ No newline at end of file diff --git a/src/RepoM.Api/IO/DefaultAppDataPathProvider.cs b/src/RepoM.Api/IO/DefaultAppDataPathProvider.cs index 62c5884e..f6070c97 100644 --- a/src/RepoM.Api/IO/DefaultAppDataPathProvider.cs +++ b/src/RepoM.Api/IO/DefaultAppDataPathProvider.cs @@ -2,6 +2,7 @@ namespace RepoM.Api.IO; using System; using System.IO; +using RepoM.Core.Plugin.Common; public class DefaultAppDataPathProvider : IAppDataPathProvider { diff --git a/src/RepoM.Api/IO/ExpressionEvaluator/RepositoryContext.cs b/src/RepoM.Api/IO/ExpressionEvaluator/RepositoryContext.cs deleted file mode 100644 index 2ac996bb..00000000 --- a/src/RepoM.Api/IO/ExpressionEvaluator/RepositoryContext.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace RepoM.Api.IO.ExpressionEvaluator; - -using System; -using System.Collections.Generic; -using System.Linq; -using RepoM.Api.Git; - -public sealed class RepositoryContext -{ - public RepositoryContext() - { - Repositories = Array.Empty(); - } - - public RepositoryContext(params Repository[] repositories) - { - Repositories = repositories.ToArray(); - } - - public RepositoryContext(IEnumerable repositories) - { - Repositories = repositories.ToArray(); - } - - public Repository[] Repositories { get; } -} \ No newline at end of file diff --git a/src/RepoM.Api/IO/ExpressionEvaluator/RepositoryExpressionEvaluator.cs b/src/RepoM.Api/IO/ExpressionEvaluator/RepositoryExpressionEvaluator.cs index 1734c71a..76c29ff5 100644 --- a/src/RepoM.Api/IO/ExpressionEvaluator/RepositoryExpressionEvaluator.cs +++ b/src/RepoM.Api/IO/ExpressionEvaluator/RepositoryExpressionEvaluator.cs @@ -6,7 +6,7 @@ namespace RepoM.Api.IO.ExpressionEvaluator; using ExpressionStringEvaluator.Methods; using ExpressionStringEvaluator.Parser; using ExpressionStringEvaluator.VariableProviders; -using RepoM.Api.Git; +using RepoM.Core.Plugin.Repository; public class RepositoryExpressionEvaluator { @@ -20,17 +20,17 @@ public RepositoryExpressionEvaluator(IEnumerable variableProv _expressionExecutor = new ExpressionExecutor(v, m); } - public string EvaluateStringExpression(string value, params Repository[] repository) + public string EvaluateStringExpression(string value, params IRepository[] repository) { return EvaluateStringExpression(value, repository.AsEnumerable()); } - public object? EvaluateValueExpression(string value, params Repository[] repository) + public object? EvaluateValueExpression(string value, params IRepository[] repository) { return EvaluateValueExpression(value, repository.AsEnumerable()); } - private object? EvaluateValueExpression(string value, IEnumerable repository) + private object? EvaluateValueExpression(string value, IEnumerable repository) { try { @@ -42,7 +42,7 @@ public string EvaluateStringExpression(string value, params Repository[] reposit } } - private string EvaluateStringExpression(string value, IEnumerable repository) + private string EvaluateStringExpression(string value, IEnumerable repository) { try { @@ -67,7 +67,7 @@ private string EvaluateStringExpression(string value, IEnumerable re } } - public bool EvaluateBooleanExpression(string? value, Repository? repository) + public bool EvaluateBooleanExpression(string? value, IRepository? repository) { if (string.IsNullOrWhiteSpace(value)) { @@ -86,7 +86,7 @@ public bool EvaluateBooleanExpression(string? value, Repository? repository) try { - Repository[] repositories = repository == null ? Array.Empty() : new[] { repository, }; + IRepository[] repositories = repository == null ? Array.Empty() : new[] { repository, }; object? result = _expressionExecutor.Execute(new RepositoryContext(repositories), value!); diff --git a/src/RepoM.Api/IO/GravellGitRepositoryFinderFactory.cs b/src/RepoM.Api/IO/GravellGitRepositoryFinderFactory.cs index 8b22d8d1..22b7846d 100644 --- a/src/RepoM.Api/IO/GravellGitRepositoryFinderFactory.cs +++ b/src/RepoM.Api/IO/GravellGitRepositoryFinderFactory.cs @@ -18,7 +18,7 @@ public GravellGitRepositoryFinderFactory(IPathSkipper pathSkipper, IFileSystem f public string Name => FACTORY_NAME; - public bool IsActive { get; } = true; + public bool IsActive => true; public IGitRepositoryFinder Create() { diff --git a/src/RepoM.Api/IO/IPathFinder.cs b/src/RepoM.Api/IO/IPathFinder.cs index 44e8d5c9..6df0a3fc 100644 --- a/src/RepoM.Api/IO/IPathFinder.cs +++ b/src/RepoM.Api/IO/IPathFinder.cs @@ -2,6 +2,7 @@ namespace RepoM.Api.IO; using System; +// todo, check original code where this was used?! public interface IPathFinder { bool CanHandle(string processName); diff --git a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionDeserializers/ActionJustTextV1Deserializer.cs b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionDeserializers/ActionJustTextV1Deserializer.cs new file mode 100644 index 00000000..a1ba9670 --- /dev/null +++ b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionDeserializers/ActionJustTextV1Deserializer.cs @@ -0,0 +1,25 @@ +namespace RepoM.Api.IO.ModuleBasedRepositoryActionProvider.ActionDeserializers; + +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data; +using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data.Actions; + +public class ActionJustTextV1Deserializer : IActionDeserializer +{ + bool IActionDeserializer.CanDeserialize(string type) + { + return "just-text@1".Equals(type, StringComparison.CurrentCultureIgnoreCase); + } + + RepositoryAction? IActionDeserializer.Deserialize(JToken jToken, ActionDeserializerComposition actionDeserializer, JsonSerializer jsonSerializer) + { + return Deserialize(jToken, jsonSerializer); + } + + private static RepositoryActionJustTextV1? Deserialize(JToken jToken, JsonSerializer jsonSerializer) + { + return jToken.ToObject(jsonSerializer); + } +} \ No newline at end of file diff --git a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionAssociateFileV1Mapper.cs b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionAssociateFileV1Mapper.cs index f0157e29..b0cea23f 100644 --- a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionAssociateFileV1Mapper.cs +++ b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionAssociateFileV1Mapper.cs @@ -8,6 +8,8 @@ namespace RepoM.Api.IO.ModuleBasedRepositoryActionProvider.ActionMappers; using RepoM.Api.Git; using RepoM.Api.IO.ExpressionEvaluator; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data.Actions; +using RepoM.Core.Plugin.Repository; +using RepoM.Core.Plugin.RepositoryActions.Actions; using RepositoryAction = RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data.RepositoryAction; public class ActionAssociateFileV1Mapper : IActionToRepositoryActionMapper @@ -70,11 +72,11 @@ IEnumerable IActionToRepositoryActionMapper.Map(Repository } } - private RepoM.Api.Git.RepositoryAction CreateProcessRunnerAction(string name, string process, string arguments = "") + private static Git.RepositoryAction CreateProcessRunnerAction(string name, string process, IRepository repository, string arguments = "") { - return new RepoM.Api.Git.RepositoryAction(name) + return new Git.RepositoryAction(name, repository) { - Action = (_, _) => ProcessHelper.StartProcess(process, arguments), + Action = new DelegateAction((_, _) => ProcessHelper.StartProcess(process, arguments)), }; } @@ -85,11 +87,11 @@ private RepoM.Api.Git.RepositoryAction CreateProcessRunnerAction(string name, st return null; } - return new RepoM.Api.Git.RepositoryAction(actionName) + return new RepoM.Api.Git.RepositoryAction(actionName, repository) { DeferredSubActionsEnumerator = () => GetFiles(repository, filePattern) - .Select(solutionFile => CreateProcessRunnerAction(Path.GetFileName(solutionFile), solutionFile)) + .Select(solutionFile => CreateProcessRunnerAction(Path.GetFileName(solutionFile), solutionFile, repository)) .ToArray(), }; } diff --git a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionBrowseRepositoryV1Mapper.cs b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionBrowseRepositoryV1Mapper.cs index 52152269..5e8e62e5 100644 --- a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionBrowseRepositoryV1Mapper.cs +++ b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionBrowseRepositoryV1Mapper.cs @@ -7,6 +7,8 @@ namespace RepoM.Api.IO.ModuleBasedRepositoryActionProvider.ActionMappers; using RepoM.Api.Git; using RepoM.Api.IO.ExpressionEvaluator; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data.Actions; +using RepoM.Core.Plugin.Repository; +using RepoM.Core.Plugin.RepositoryActions.Actions; public class ActionBrowseRepositoryV1Mapper : IActionToRepositoryActionMapper { @@ -70,23 +72,23 @@ private IEnumerable Map(RepositoryActionBrowseRepositoryV1? ac if (repository.Remotes.Count == 1 || forceSingle) { - return CreateProcessRunnerAction(actionName, repository.Remotes[0].Url); + return CreateProcessRunnerAction(actionName, repository.Remotes[0].Url, repository); } - return new RepositoryAction(actionName) + return new RepositoryAction(actionName, repository) { DeferredSubActionsEnumerator = () => repository.Remotes .Take(50) - .Select(remote => CreateProcessRunnerAction(remote.Name, remote.Url)) + .Select(remote => CreateProcessRunnerAction(remote.Name, remote.Url, repository)) .ToArray(), }; } - private RepositoryAction CreateProcessRunnerAction(string name, string process, string arguments = "") + private static RepositoryAction CreateProcessRunnerAction(string name, string process, IRepository repository, string arguments = "") { - return new RepositoryAction(name) + return new RepositoryAction(name, repository) { - Action = (_, _) => ProcessHelper.StartProcess(process, arguments), + Action = new DelegateAction((_, _) => ProcessHelper.StartProcess(process, arguments)), }; } } \ No newline at end of file diff --git a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionBrowserV1Mapper.cs b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionBrowserV1Mapper.cs index 88be83b1..dbb0b082 100644 --- a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionBrowserV1Mapper.cs +++ b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionBrowserV1Mapper.cs @@ -7,6 +7,7 @@ namespace RepoM.Api.IO.ModuleBasedRepositoryActionProvider.ActionMappers; using RepoM.Api.Git; using RepoM.Api.IO.ExpressionEvaluator; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data.Actions; +using RepoM.Core.Plugin.RepositoryActions.Actions; using RepositoryAction = RepoM.Api.Git.RepositoryAction; public class ActionBrowserV1Mapper : IActionToRepositoryActionMapper @@ -54,9 +55,9 @@ private IEnumerable Map(RepositoryActionBrowserV1? action, Rep var name = NameHelper.EvaluateName(action.Name, repository, _translationService, _expressionEvaluator); var url = _expressionEvaluator.EvaluateStringExpression(action.Url, repository); - yield return new RepositoryAction(name) + yield return new RepositoryAction(name, repository) { - Action = (_, _) => ProcessHelper.StartProcess(url, string.Empty), + Action = new DelegateAction((_, _) => ProcessHelper.StartProcess(url, string.Empty)), }; } } \ No newline at end of file diff --git a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionCommandV1Mapper.cs b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionCommandV1Mapper.cs index b6f005d4..0d2622b7 100644 --- a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionCommandV1Mapper.cs +++ b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionCommandV1Mapper.cs @@ -7,6 +7,7 @@ namespace RepoM.Api.IO.ModuleBasedRepositoryActionProvider.ActionMappers; using RepoM.Api.Git; using RepoM.Api.IO.ExpressionEvaluator; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data.Actions; +using RepoM.Core.Plugin.RepositoryActions.Actions; using RepositoryAction = RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data.RepositoryAction; public class ActionCommandV1Mapper : IActionToRepositoryActionMapper @@ -35,7 +36,7 @@ IEnumerable IActionToRepositoryActionMapper.Map(Repository return Map(action as RepositoryActionCommandV1, repository.First()); } - private IEnumerable Map(RepositoryActionCommandV1? action, Repository repository) + private IEnumerable Map(RepositoryActionCommandV1? action, Repository repository) { if (action == null) { @@ -51,9 +52,9 @@ IEnumerable IActionToRepositoryActionMapper.Map(Repository var command = _expressionEvaluator.EvaluateStringExpression(action.Command ?? string.Empty, repository); var arguments = _expressionEvaluator.EvaluateStringExpression(action.Arguments ?? string.Empty, repository); - yield return new RepoM.Api.Git.RepositoryAction(name) + yield return new Git.RepositoryAction(name, repository) { - Action = (_, _) => ProcessHelper.StartProcess(command, arguments), + Action = new DelegateAction((_, _) => ProcessHelper.StartProcess(command, arguments)), }; } } \ No newline at end of file diff --git a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionExecutableV1Mapper.cs b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionExecutableV1Mapper.cs index 914ab07c..e4a8b810 100644 --- a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionExecutableV1Mapper.cs +++ b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionExecutableV1Mapper.cs @@ -8,6 +8,7 @@ namespace RepoM.Api.IO.ModuleBasedRepositoryActionProvider.ActionMappers; using RepoM.Api.Git; using RepoM.Api.IO.ExpressionEvaluator; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data.Actions; +using RepoM.Core.Plugin.RepositoryActions.Actions; using RepositoryAction = RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data.RepositoryAction; public class ActionExecutableV1Mapper : IActionToRepositoryActionMapper @@ -76,9 +77,9 @@ IEnumerable IActionToRepositoryActionMapper.Map(Repository arguments = _expressionEvaluator.EvaluateStringExpression(action.Arguments, repository); } - yield return new RepoM.Api.Git.RepositoryAction(name) + yield return new Git.RepositoryAction(name, repository) { - Action = (_, _) => ProcessHelper.StartProcess(normalized, arguments), + Action = new DelegateAction((_, _) => ProcessHelper.StartProcess(normalized, arguments)) }; found = true; } diff --git a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionFolderV1Mapper.cs b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionFolderV1Mapper.cs index 6cf0d938..c423824c 100644 --- a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionFolderV1Mapper.cs +++ b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionFolderV1Mapper.cs @@ -57,7 +57,7 @@ IEnumerable IActionToRepositoryActionMapper.Map(Repository if (deferred) { - yield return new RepoM.Api.Git.RepositoryAction(name) + yield return new RepoM.Api.Git.RepositoryAction(name, repository) { CanExecute = true, DeferredSubActionsEnumerator = () => @@ -69,7 +69,7 @@ IEnumerable IActionToRepositoryActionMapper.Map(Repository } else { - yield return new RepoM.Api.Git.RepositoryAction(name) + yield return new RepoM.Api.Git.RepositoryAction(name, repository) { CanExecute = true, SubActions = action.Items diff --git a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionGitCheckoutV1Mapper.cs b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionGitCheckoutV1Mapper.cs index b1c9f3ad..052b69e0 100644 --- a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionGitCheckoutV1Mapper.cs +++ b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionGitCheckoutV1Mapper.cs @@ -7,6 +7,7 @@ namespace RepoM.Api.IO.ModuleBasedRepositoryActionProvider.ActionMappers; using RepoM.Api.Git; using RepoM.Api.IO.ExpressionEvaluator; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data.Actions; +using RepoM.Core.Plugin.RepositoryActions.Actions; using RepositoryAction = Data.RepositoryAction; public class ActionGitCheckoutV1Mapper : IActionToRepositoryActionMapper @@ -60,28 +61,28 @@ private IEnumerable Map(RepositoryActionGitCheckoutV1? act name = _translationService.Translate("Checkout"); } - yield return new Git.RepositoryAction(_translationService.Translate("Checkout")) + yield return new Git.RepositoryAction(_translationService.Translate("Checkout"), repository) { DeferredSubActionsEnumerator = () => repository.LocalBranches .Take(50) - .Select(branch => new Git.RepositoryAction(branch) + .Select(branch => new Git.RepositoryAction(branch, repository) { - Action = (_, _) => _repositoryWriter.Checkout(repository, branch), + Action = new DelegateAction((_, _) => _repositoryWriter.Checkout(repository, branch)), CanExecute = !repository.CurrentBranch.Equals(branch, StringComparison.OrdinalIgnoreCase), }) .Union(new RepositoryActionBase[] { - new RepositorySeparatorAction(), // doesn't work todo - new Git.RepositoryAction(_translationService.Translate("Remote branches")) + new RepositorySeparatorAction(repository), // doesn't work todo + new Git.RepositoryAction(_translationService.Translate("Remote branches"), repository) { DeferredSubActionsEnumerator = () => { Git.RepositoryAction[] remoteBranches = repository .ReadAllBranches() - .Select(branch => new Git.RepositoryAction(branch) + .Select(branch => new Git.RepositoryAction(branch, repository) { - Action = (_, _) => _repositoryWriter.Checkout(repository, branch), + Action = new DelegateAction((_, _) => _repositoryWriter.Checkout(repository, branch)), CanExecute = !repository.CurrentBranch.Equals(branch, StringComparison.OrdinalIgnoreCase), }) .ToArray(); @@ -93,11 +94,11 @@ private IEnumerable Map(RepositoryActionGitCheckoutV1? act return new RepositoryActionBase[] { - new Git.RepositoryAction(_translationService.Translate("No remote branches found")) + new Git.RepositoryAction(_translationService.Translate("No remote branches found"), repository) { CanExecute = false, }, - new Git.RepositoryAction(_translationService.Translate("Try to fetch changes if you're expecting remote branches")) + new Git.RepositoryAction(_translationService.Translate("Try to fetch changes if you're expecting remote branches"), repository) { CanExecute = false, }, diff --git a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionJustTextV1Mapper.cs b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionJustTextV1Mapper.cs new file mode 100644 index 00000000..80a419b5 --- /dev/null +++ b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionJustTextV1Mapper.cs @@ -0,0 +1,59 @@ +namespace RepoM.Api.IO.ModuleBasedRepositoryActionProvider.ActionMappers; + +using System; +using System.Collections.Generic; +using System.Linq; +using RepoM.Api.Common; +using RepoM.Api.Git; +using RepoM.Api.IO.ExpressionEvaluator; +using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data.Actions; +using RepoM.Core.Plugin.RepositoryActions.Actions; +using RepositoryAction = RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data.RepositoryAction; + +public class ActionJustTextV1Mapper : IActionToRepositoryActionMapper +{ + private readonly RepositoryExpressionEvaluator _expressionEvaluator; + private readonly ITranslationService _translationService; + + public ActionJustTextV1Mapper(RepositoryExpressionEvaluator expressionEvaluator, ITranslationService translationService) + { + _expressionEvaluator = expressionEvaluator ?? throw new ArgumentNullException(nameof(expressionEvaluator)); + _translationService = translationService ?? throw new ArgumentNullException(nameof(translationService)); + } + + bool IActionToRepositoryActionMapper.CanMap(RepositoryAction action) + { + return action is RepositoryActionJustTextV1; + } + + public bool CanHandleMultipleRepositories() + { + return false; + } + + IEnumerable IActionToRepositoryActionMapper.Map(RepositoryAction action, IEnumerable repository, ActionMapperComposition actionMapperComposition) + { + return Map(action as RepositoryActionJustTextV1, repository.First()); + } + + private IEnumerable Map(RepositoryActionJustTextV1? action, Repository repository) + { + if (action == null) + { + yield break; + } + + if (!_expressionEvaluator.EvaluateBooleanExpression(action.Active, repository)) + { + yield break; + } + + var name = NameHelper.EvaluateName(action.Name, repository, _translationService, _expressionEvaluator); + + yield return new Git.RepositoryAction(name, repository) + { + Action = NullAction.Instance, + CanExecute = _expressionEvaluator.EvaluateBooleanExpression(action.Enabled, repository), + }; + } +} \ No newline at end of file diff --git a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionPinRepositoryV1Mapper.cs b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionPinRepositoryV1Mapper.cs index 779a4360..5f8a1121 100644 --- a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionPinRepositoryV1Mapper.cs +++ b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionPinRepositoryV1Mapper.cs @@ -7,6 +7,7 @@ namespace RepoM.Api.IO.ModuleBasedRepositoryActionProvider.ActionMappers; using RepoM.Api.Git; using RepoM.Api.IO.ExpressionEvaluator; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data.Actions; +using RepoM.Core.Plugin.RepositoryActions.Actions; using RepositoryAction = RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data.RepositoryAction; public class ActionPinRepositoryV1Mapper : IActionToRepositoryActionMapper @@ -85,9 +86,9 @@ private IEnumerable Map(RepositoryActionPinRepositoryV1? a Git.RepositoryAction CreateAction(string name, Repository repository, bool newPinnedValue) { - return new Git.RepositoryAction(name) + return new Git.RepositoryAction(name, repository) { - Action = (_, _) => _repositoryMonitor.SetPinned(newPinnedValue, repository), + Action = new DelegateAction((_, _) => _repositoryMonitor.SetPinned(newPinnedValue, repository)), }; } diff --git a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionSeparatorV1Mapper.cs b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionSeparatorV1Mapper.cs index 5c0cd747..74122548 100644 --- a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionSeparatorV1Mapper.cs +++ b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/ActionMappers/ActionSeparatorV1Mapper.cs @@ -53,6 +53,6 @@ private IEnumerable Map(RepositoryActionSeparatorV1? actio yield break; } - yield return new RepositorySeparatorAction(); + yield return new RepositorySeparatorAction(repository); } } \ No newline at end of file diff --git a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/Data/Actions/RepositoryActionJustTextV1.cs b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/Data/Actions/RepositoryActionJustTextV1.cs new file mode 100644 index 00000000..68f37a78 --- /dev/null +++ b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/Data/Actions/RepositoryActionJustTextV1.cs @@ -0,0 +1,6 @@ +namespace RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data.Actions; + +public class RepositoryActionJustTextV1 : RepositoryAction +{ + public string? Enabled { get; set; } +} \ No newline at end of file diff --git a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/RepositorySpecificConfiguration.cs b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/RepositorySpecificConfiguration.cs index 83151597..2fff680f 100644 --- a/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/RepositorySpecificConfiguration.cs +++ b/src/RepoM.Api/IO/ModuleBasedRepositoryActionProvider/RepositorySpecificConfiguration.cs @@ -16,6 +16,9 @@ namespace RepoM.Api.IO.ModuleBasedRepositoryActionProvider; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Deserialization; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Exceptions; using RepoM.Api.IO.Variables; +using RepoM.Core.Plugin.Common; +using RepoM.Core.Plugin.RepositoryActions.Actions; +using IRepository = RepoM.Core.Plugin.Repository.IRepository; using Repository = RepoM.Api.Git.Repository; using RepositoryAction = RepoM.Api.Git.RepositoryAction; @@ -65,14 +68,14 @@ private string GetRepositoryActionsFilename(string basePath) throw new ConfigurationFileNotFoundException(failingFilename); } - public (Dictionary? envVars, List? Variables, List? actions, List? tags) Get(params Repository[] repositories) + public (Dictionary? envVars, List? Variables, List? actions, List? tags) Get(params IRepository[] repositories) { if (!repositories.Any()) { return (null, null, null, null); } - Repository? repository = repositories.FirstOrDefault(); //todo + IRepository? repository = repositories.FirstOrDefault(); //todo if (repository == null) { return (null, null, null, null); @@ -250,19 +253,19 @@ List EvaluateVariables(IEnumerable? vars) return (envVars, variables, actions, tags); } - private object? Evaluate(object? input, Repository? repository) + private object? Evaluate(object? input, IRepository? repository) { if (input is not string s) { return input; } - Repository[] repositories = repository == null ? Array.Empty() : new[] { repository, }; + IRepository[] repositories = repository == null ? Array.Empty() : new[] { repository, }; return _repoExpressionEvaluator.EvaluateValueExpression(s, repositories); } - private string EvaluateString(string? input, Repository? repository) + private string EvaluateString(string? input, IRepository? repository) { object? v = Evaluate(input, repository); if (v == null) @@ -273,7 +276,7 @@ private string EvaluateString(string? input, Repository? repository) return v.ToString(); } - private bool IsEnabled(string? booleanExpression, bool defaultWhenNullOrEmpty, Repository? repository) + private bool IsEnabled(string? booleanExpression, bool defaultWhenNullOrEmpty, IRepository? repository) { return string.IsNullOrWhiteSpace(booleanExpression) ? defaultWhenNullOrEmpty @@ -319,7 +322,7 @@ public IEnumerable GetTags(Repository repository) return GetTagsInner(repository).Distinct(); } - private IEnumerable GetTagsInner(Repository repository) + private IEnumerable GetTagsInner(IRepository repository) { List EvaluateVariables(IEnumerable? vars) { @@ -374,7 +377,7 @@ List EvaluateVariables(IEnumerable? vars) } } - private object? Evaluate(object? input, Repository repository) + private object? Evaluate(object? input, IRepository repository) { if (input is string s) { @@ -384,7 +387,7 @@ List EvaluateVariables(IEnumerable? vars) return input; } - private bool IsEnabled(string? booleanExpression, bool defaultWhenNullOrEmpty, Repository repository) + private bool IsEnabled(string? booleanExpression, bool defaultWhenNullOrEmpty, IRepository repository) { return string.IsNullOrWhiteSpace(booleanExpression) ? defaultWhenNullOrEmpty @@ -445,14 +448,14 @@ public IEnumerable CreateActions(params Repository[] repos { if (ex is ConfigurationFileNotFoundException configurationFileNotFoundException) { - foreach (RepositoryAction failingItem in CreateFailing(configurationFileNotFoundException, configurationFileNotFoundException.Filename)) + foreach (RepositoryAction failingItem in CreateFailing(configurationFileNotFoundException, configurationFileNotFoundException.Filename, singleRepository!)) // todo coenm { yield return failingItem; } } else { - foreach (RepositoryAction failingItem in CreateFailing(ex, null)) + foreach (RepositoryAction failingItem in CreateFailing(ex, null, singleRepository!)) // todo coenm { yield return failingItem; } @@ -509,23 +512,30 @@ private List EvaluateVariables(IEnumerable? vars, R .ToList(); } - private IEnumerable CreateFailing(Exception ex, string? filename) + private IEnumerable CreateFailing(Exception ex, string? filename, IRepository repository) { - yield return new RepositoryAction(_translationService.Translate("Could not read repository actions")) + yield return new RepositoryAction(_translationService.Translate("Could not read repository actions"), repository) { CanExecute = false, }; - yield return new RepositoryAction(ex.Message) + yield return new RepositoryAction(ex.Message, repository) { CanExecute = false, }; if (!string.IsNullOrWhiteSpace(filename)) { - yield return new RepositoryAction(_translationService.Translate("Fix")) + yield return new RepositoryAction(_translationService.Translate("Fix"), repository) { - Action = (_, _) => ProcessHelper.StartProcess(_fileSystem.Path.GetDirectoryName(filename), string.Empty), + Action = new DelegateAction((_, _) => + { + var directoryName = _fileSystem.Path.GetDirectoryName(filename); + if (directoryName != null) + { + ProcessHelper.StartProcess(directoryName, string.Empty); + } + }), }; } } diff --git a/src/RepoM.Api/IO/MultipleRepositoryActionHelper.cs b/src/RepoM.Api/IO/MultipleRepositoryActionHelper.cs index 26b6f237..06260fce 100644 --- a/src/RepoM.Api/IO/MultipleRepositoryActionHelper.cs +++ b/src/RepoM.Api/IO/MultipleRepositoryActionHelper.cs @@ -4,6 +4,7 @@ namespace RepoM.Api.IO; using System.Collections.Generic; using System.Linq; using RepoM.Api.Git; +using RepoM.Core.Plugin.RepositoryActions.Actions; public static class MultipleRepositoryActionHelper { @@ -13,9 +14,10 @@ public static RepositoryAction CreateActionForMultipleRepositories( Action action, bool executionCausesSynchronizing = false) { - return new RepositoryAction(name) + // todo coen, fix + return new RepositoryAction(name, repositories.First()) { - Action = (_, _) => + Action = new DelegateAction((_, _) => { // copy over to an array to not get an exception // once the enumerator changes (which can happen when a change @@ -26,7 +28,7 @@ public static RepositoryAction CreateActionForMultipleRepositories( { SafelyExecute(action, repository); // git/io-exceptions will break the loop, put in try/catch } - }, + }), ExecutionCausesSynchronizing = executionCausesSynchronizing, }; } diff --git a/src/RepoM.Api/IO/VariableProviders/CustomEnvironmentVariableVariableProvider.cs b/src/RepoM.Api/IO/VariableProviders/CustomEnvironmentVariableVariableProvider.cs index c6090db7..6522f332 100644 --- a/src/RepoM.Api/IO/VariableProviders/CustomEnvironmentVariableVariableProvider.cs +++ b/src/RepoM.Api/IO/VariableProviders/CustomEnvironmentVariableVariableProvider.cs @@ -5,9 +5,8 @@ namespace RepoM.Api.IO.VariableProviders; using System.Linq; using ExpressionStringEvaluator.VariableProviders; using JetBrains.Annotations; -using RepoM.Api.IO.ExpressionEvaluator; using RepoM.Api.IO.Variables; -using Repository = Git.Repository; +using RepoM.Core.Plugin.Repository; [UsedImplicitly] public class CustomEnvironmentVariableVariableProvider : IVariableProvider @@ -38,7 +37,7 @@ public bool CanProvide(string key) var prefixLength = PREFIX.Length; var envKey = key.Substring(prefixLength, key.Length - prefixLength); - Repository? singleContext = context.Repositories.SingleOrDefault(); + IRepository? singleContext = context.Repositories.SingleOrDefault(); if (singleContext == null) { @@ -63,7 +62,7 @@ public bool CanProvide(string key) return result; } - private static Dictionary GetRepoEnvironmentVariables(Repository repository) + private static Dictionary GetRepoEnvironmentVariables(IRepository repository) { return EnvironmentVariableStore.Get(repository); } diff --git a/src/RepoM.Api/IO/VariableProviders/RepoMVariableProvider.cs b/src/RepoM.Api/IO/VariableProviders/RepoMVariableProvider.cs index b97bbd4c..78522215 100644 --- a/src/RepoM.Api/IO/VariableProviders/RepoMVariableProvider.cs +++ b/src/RepoM.Api/IO/VariableProviders/RepoMVariableProvider.cs @@ -1,4 +1,3 @@ - namespace RepoM.Api.IO.VariableProviders; using System; diff --git a/src/RepoM.Api/IO/VariableProviders/RepositoryVariableProvider.cs b/src/RepoM.Api/IO/VariableProviders/RepositoryVariableProvider.cs index 96c6a295..579a0cf8 100644 --- a/src/RepoM.Api/IO/VariableProviders/RepositoryVariableProvider.cs +++ b/src/RepoM.Api/IO/VariableProviders/RepositoryVariableProvider.cs @@ -3,8 +3,7 @@ namespace RepoM.Api.IO.VariableProviders; using System; using System.Linq; using ExpressionStringEvaluator.VariableProviders; -using RepoM.Api.Git; -using RepoM.Api.IO.ExpressionEvaluator; +using RepoM.Core.Plugin.Repository; public class RepositoryVariableProvider : IVariableProvider { @@ -25,7 +24,7 @@ public bool CanProvide(string key) private static string ProvideString(RepositoryContext context, string key, string? arg) { - Repository? repository = context.Repositories.SingleOrDefault(); + IRepository? repository = context.Repositories.SingleOrDefault(); if (repository == null) { return string.Empty; diff --git a/src/RepoM.Api/IO/VariableProviders/VariableProviderAdapter.cs b/src/RepoM.Api/IO/VariableProviders/VariableProviderAdapter.cs new file mode 100644 index 00000000..d5c15965 --- /dev/null +++ b/src/RepoM.Api/IO/VariableProviders/VariableProviderAdapter.cs @@ -0,0 +1,52 @@ +namespace RepoM.Api.IO.VariableProviders; + +using System; +using System.Collections.Generic; +using System.Linq; +using Core.Plugin.Repository; +using JetBrains.Annotations; +using PluginVariableProvider = Core.Plugin.VariableProviders.IVariableProvider; +using PluginRepositoryContextVariableProvider = Core.Plugin.VariableProviders.IVariableProvider; + +[UsedImplicitly] +public class VariableProviderAdapter : ExpressionStringEvaluator.VariableProviders.IVariableProvider +{ + private readonly PluginVariableProvider[] _variableProviderImplementation; + + public VariableProviderAdapter(IEnumerable variableProviders) + { + _variableProviderImplementation = variableProviders.ToArray(); + } + + public bool CanProvide(string key) + { + return GetProvider(key) != null; + } + + public object? Provide(string key, string? arg) + { + throw new NotImplementedException(); + } + + public object? Provide(RepositoryContext context, string key, string? arg) + { + PluginVariableProvider? instance = GetProvider(key); + + if (instance == null) + { + throw new NullReferenceException(); + } + + if (instance is PluginRepositoryContextVariableProvider typedInstance) + { + return typedInstance.Provide(context, key, arg); + } + + return instance.Provide(key, arg); + } + + private PluginVariableProvider? GetProvider(string key) + { + return _variableProviderImplementation.FirstOrDefault(x => x.CanProvide(key)); + } +} \ No newline at end of file diff --git a/src/RepoM.Api/IO/Variables/EnvironmentVariableStore.cs b/src/RepoM.Api/IO/Variables/EnvironmentVariableStore.cs index 45ae9b6b..b236be2d 100644 --- a/src/RepoM.Api/IO/Variables/EnvironmentVariableStore.cs +++ b/src/RepoM.Api/IO/Variables/EnvironmentVariableStore.cs @@ -3,7 +3,7 @@ namespace RepoM.Api.IO.Variables; using System; using System.Collections.Generic; using System.Threading; -using RepoM.Api.Git; +using RepoM.Core.Plugin.Repository; public static class EnvironmentVariableStore { @@ -20,7 +20,7 @@ public static IDisposable Set(Dictionary? envVars) return new ExecuteOnDisposed(() => _envVars.Value = new Dictionary()); } - public static Dictionary Get(Repository _) + public static Dictionary Get(IRepository _) { return _envVars.Value ?? new Dictionary(0); } diff --git a/src/RepoM.Api/Ordering/Az/AlphabetComparerConfigurationV1.cs b/src/RepoM.Api/Ordering/Az/AlphabetComparerConfigurationV1.cs new file mode 100644 index 00000000..fc060a88 --- /dev/null +++ b/src/RepoM.Api/Ordering/Az/AlphabetComparerConfigurationV1.cs @@ -0,0 +1,10 @@ +namespace RepoM.Api.Ordering.Az; + +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +public class AlphabetComparerConfigurationV1 : IRepositoriesComparerConfiguration +{ + public string? Property { get; set; } + + public int Weight { get; set; } +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/Az/AlphabetComparerConfigurationV1Registration.cs b/src/RepoM.Api/Ordering/Az/AlphabetComparerConfigurationV1Registration.cs new file mode 100644 index 00000000..f7a388b3 --- /dev/null +++ b/src/RepoM.Api/Ordering/Az/AlphabetComparerConfigurationV1Registration.cs @@ -0,0 +1,11 @@ +namespace RepoM.Api.Ordering.Az; + +using System; +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +public class AlphabetComparerConfigurationV1Registration : IConfigurationRegistration +{ + public Type ConfigurationType { get; } = typeof(AlphabetComparerConfigurationV1); + + public string Tag => "az-comparer@1"; +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/Az/AzComparer.cs b/src/RepoM.Api/Ordering/Az/AzComparer.cs new file mode 100644 index 00000000..5b667c86 --- /dev/null +++ b/src/RepoM.Api/Ordering/Az/AzComparer.cs @@ -0,0 +1,58 @@ +namespace RepoM.Api.Ordering.Az; + +using System; +using RepoM.Core.Plugin.Repository; +using RepoM.Core.Plugin.RepositoryOrdering; + +public class AzComparer : IRepositoryComparer +{ + private readonly int _weight; + private readonly string? _property; + + public AzComparer(int weight, string? property) + { + _weight = weight; + _property = property; + } + + public int Compare(IRepository? x, IRepository? y) + { + if (ReferenceEquals(x, y)) + { + return 0; + } + + if (y is null) + { + return _weight; + } + + if (x is null) + { + return -1 * _weight; + } + + var comparisonValue = 0; + + if ("Name".Equals(_property, StringComparison.InvariantCultureIgnoreCase)) + { + comparisonValue = string.Compare(x.Name, y.Name, StringComparison.Ordinal); + } + else if ("Location".Equals(_property, StringComparison.InvariantCultureIgnoreCase)) + { + comparisonValue = string.Compare(x.Location, y.Location, StringComparison.Ordinal); + } + + if (comparisonValue < 0) + { + return -1 * _weight; + } + + if (comparisonValue == 0) + { + return 0; + } + + return _weight; + } +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/Az/AzRepositoryComparerFactory.cs b/src/RepoM.Api/Ordering/Az/AzRepositoryComparerFactory.cs new file mode 100644 index 00000000..0e90121a --- /dev/null +++ b/src/RepoM.Api/Ordering/Az/AzRepositoryComparerFactory.cs @@ -0,0 +1,11 @@ +namespace RepoM.Api.Ordering.Az; + +using RepoM.Core.Plugin.RepositoryOrdering; + +public class AzRepositoryComparerFactory : IRepositoryComparerFactory +{ + public IRepositoryComparer Create(AlphabetComparerConfigurationV1 configuration) + { + return new AzComparer(configuration.Weight, configuration.Property); + } +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/Composition/CompositionComparer.cs b/src/RepoM.Api/Ordering/Composition/CompositionComparer.cs new file mode 100644 index 00000000..274b2669 --- /dev/null +++ b/src/RepoM.Api/Ordering/Composition/CompositionComparer.cs @@ -0,0 +1,36 @@ +namespace RepoM.Api.Ordering.Composition; + +using System.Collections.Generic; +using System.Linq; +using RepoM.Core.Plugin.Repository; +using RepoM.Core.Plugin.RepositoryOrdering; + +public class CompositionComparer : IRepositoryComparer +{ + private readonly IRepositoryComparer[] _comparers; + + public CompositionComparer(IEnumerable comparers) + { + _comparers = comparers.ToArray(); + } + + public int Compare(IRepository? x, IRepository? y) + { + if (ReferenceEquals(x, y)) + { + return 0; + } + + if (y is null) + { + return 1; + } + + if (x is null) + { + return -1; + } + + return _comparers.Select(c => c.Compare(x, y)).FirstOrDefault(result => result != 0); + } +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/Composition/CompositionComparerConfigurationV1.cs b/src/RepoM.Api/Ordering/Composition/CompositionComparerConfigurationV1.cs new file mode 100644 index 00000000..3069717a --- /dev/null +++ b/src/RepoM.Api/Ordering/Composition/CompositionComparerConfigurationV1.cs @@ -0,0 +1,9 @@ +namespace RepoM.Api.Ordering.Composition; + +using System.Collections.Generic; +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +public class CompositionComparerConfigurationV1 : IRepositoriesComparerConfiguration +{ + public List Comparers { get; set; } = new(); +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/Composition/CompositionComparerConfigurationV1Registration.cs b/src/RepoM.Api/Ordering/Composition/CompositionComparerConfigurationV1Registration.cs new file mode 100644 index 00000000..933733f7 --- /dev/null +++ b/src/RepoM.Api/Ordering/Composition/CompositionComparerConfigurationV1Registration.cs @@ -0,0 +1,11 @@ +namespace RepoM.Api.Ordering.Composition; + +using System; +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +public class CompositionComparerConfigurationV1Registration : IConfigurationRegistration +{ + public Type ConfigurationType { get; } = typeof(CompositionComparerConfigurationV1); + + public string Tag => "composition@1"; +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/Composition/CompositionRepositoryComparerFactory.cs b/src/RepoM.Api/Ordering/Composition/CompositionRepositoryComparerFactory.cs new file mode 100644 index 00000000..5ae40434 --- /dev/null +++ b/src/RepoM.Api/Ordering/Composition/CompositionRepositoryComparerFactory.cs @@ -0,0 +1,20 @@ +namespace RepoM.Api.Ordering.Composition; + +using System; +using System.Linq; +using RepoM.Core.Plugin.RepositoryOrdering; + +public class CompositionRepositoryComparerFactory : IRepositoryComparerFactory +{ + private readonly IRepositoryComparerFactory _factory; + + public CompositionRepositoryComparerFactory(IRepositoryComparerFactory factory) + { + _factory = factory ?? throw new ArgumentNullException(nameof(factory)); + } + + public IRepositoryComparer Create(CompositionComparerConfigurationV1 configuration) + { + return new CompositionComparer(configuration.Comparers.Select(c => _factory.Create(c))); + } +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/IsPinned/IsPinnedScoreCalculator.cs b/src/RepoM.Api/Ordering/IsPinned/IsPinnedScoreCalculator.cs new file mode 100644 index 00000000..bacc4818 --- /dev/null +++ b/src/RepoM.Api/Ordering/IsPinned/IsPinnedScoreCalculator.cs @@ -0,0 +1,28 @@ +namespace RepoM.Api.Ordering.IsPinned; + +using System; +using RepoM.Api.Git; +using RepoM.Core.Plugin.Repository; +using RepoM.Core.Plugin.RepositoryOrdering; + +public class IsPinnedScoreCalculator : IRepositoryScoreCalculator +{ + private readonly IRepositoryMonitor _monitor; + private readonly int _weight; + + public IsPinnedScoreCalculator(IRepositoryMonitor monitor, int weight) + { + _monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); + _weight = weight; + } + + public int Score(IRepository repository) + { + if (repository is Repository r) + { + return _monitor.IsPinned(r) ? _weight : 0; + } + + return 0; + } +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/IsPinned/IsPinnedScorerConfigurationV1.cs b/src/RepoM.Api/Ordering/IsPinned/IsPinnedScorerConfigurationV1.cs new file mode 100644 index 00000000..fec738a7 --- /dev/null +++ b/src/RepoM.Api/Ordering/IsPinned/IsPinnedScorerConfigurationV1.cs @@ -0,0 +1,8 @@ +namespace RepoM.Api.Ordering.IsPinned; + +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +public class IsPinnedScorerConfigurationV1 : IRepositoryScorerConfiguration +{ + public int Weight { get; set; } +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/IsPinned/IsPinnedScorerConfigurationV1Registration.cs b/src/RepoM.Api/Ordering/IsPinned/IsPinnedScorerConfigurationV1Registration.cs new file mode 100644 index 00000000..3abfa329 --- /dev/null +++ b/src/RepoM.Api/Ordering/IsPinned/IsPinnedScorerConfigurationV1Registration.cs @@ -0,0 +1,13 @@ +namespace RepoM.Api.Ordering.IsPinned; + +using System; +using JetBrains.Annotations; +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +[UsedImplicitly] +public class IsPinnedScorerConfigurationV1Registration : IConfigurationRegistration +{ + public Type ConfigurationType { get; } = typeof(IsPinnedScorerConfigurationV1); + + public string Tag => "is-pinned-scorer@1"; +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/IsPinned/IsPinnedScorerFactory.cs b/src/RepoM.Api/Ordering/IsPinned/IsPinnedScorerFactory.cs new file mode 100644 index 00000000..60fb827c --- /dev/null +++ b/src/RepoM.Api/Ordering/IsPinned/IsPinnedScorerFactory.cs @@ -0,0 +1,20 @@ +namespace RepoM.Api.Ordering.IsPinned; + +using System; +using RepoM.Api.Git; +using RepoM.Core.Plugin.RepositoryOrdering; + +public class IsPinnedScorerFactory : IRepositoryScoreCalculatorFactory +{ + private readonly IRepositoryMonitor _monitor; + + public IsPinnedScorerFactory(IRepositoryMonitor monitor) + { + _monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); + } + + public IRepositoryScoreCalculator Create(IsPinnedScorerConfigurationV1 config) + { + return new IsPinnedScoreCalculator(_monitor, config.Weight); + } +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/Label/TagScoreCalculator.cs b/src/RepoM.Api/Ordering/Label/TagScoreCalculator.cs new file mode 100644 index 00000000..2c6627c1 --- /dev/null +++ b/src/RepoM.Api/Ordering/Label/TagScoreCalculator.cs @@ -0,0 +1,22 @@ +namespace RepoM.Api.Ordering.Label; + +using System.Linq; +using RepoM.Core.Plugin.Repository; +using RepoM.Core.Plugin.RepositoryOrdering; + +public class TagScoreCalculator : IRepositoryScoreCalculator +{ + private readonly string _tag; + private readonly int _weight; + + public TagScoreCalculator(string tag, int weight) + { + _tag = tag; + _weight = weight; + } + + public int Score(IRepository repository) + { + return repository.Tags.Contains(_tag) ? _weight : 0; + } +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/Label/TagScorerConfigurationV1.cs b/src/RepoM.Api/Ordering/Label/TagScorerConfigurationV1.cs new file mode 100644 index 00000000..8b6efb7d --- /dev/null +++ b/src/RepoM.Api/Ordering/Label/TagScorerConfigurationV1.cs @@ -0,0 +1,10 @@ +namespace RepoM.Api.Ordering.Label; + +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +public class TagScorerConfigurationV1 : IRepositoryScorerConfiguration +{ + public int Weight { get; set; } + + public string? Tag { get; set; } +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/Label/TagScorerConfigurationV1Registration.cs b/src/RepoM.Api/Ordering/Label/TagScorerConfigurationV1Registration.cs new file mode 100644 index 00000000..c9a2f9e9 --- /dev/null +++ b/src/RepoM.Api/Ordering/Label/TagScorerConfigurationV1Registration.cs @@ -0,0 +1,11 @@ +namespace RepoM.Api.Ordering.Label; + +using System; +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +public class TagScorerConfigurationV1Registration : IConfigurationRegistration +{ + public Type ConfigurationType { get; } = typeof(TagScorerConfigurationV1); + + public string Tag => "tag-scorer@1"; +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/Label/TagScorerFactory.cs b/src/RepoM.Api/Ordering/Label/TagScorerFactory.cs new file mode 100644 index 00000000..e2491fb3 --- /dev/null +++ b/src/RepoM.Api/Ordering/Label/TagScorerFactory.cs @@ -0,0 +1,17 @@ +namespace RepoM.Api.Ordering.Label; + +using System; +using RepoM.Core.Plugin.RepositoryOrdering; + +public class TagScorerFactory : IRepositoryScoreCalculatorFactory +{ + public IRepositoryScoreCalculator Create(TagScorerConfigurationV1 config) + { + if (string.IsNullOrWhiteSpace(config.Tag)) + { + throw new Exception("Tag cannot be null"); + } + + return new TagScoreCalculator(config.Tag!, config.Weight); + } +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/Score/ScoreComparer.cs b/src/RepoM.Api/Ordering/Score/ScoreComparer.cs new file mode 100644 index 00000000..db1adf72 --- /dev/null +++ b/src/RepoM.Api/Ordering/Score/ScoreComparer.cs @@ -0,0 +1,36 @@ +namespace RepoM.Api.Ordering.Score; + +using System; +using RepoM.Core.Plugin.Repository; +using RepoM.Core.Plugin.RepositoryOrdering; + +public class ScoreComparer : IRepositoryComparer +{ + private readonly IRepositoryScoreCalculator _calculator; + + public ScoreComparer(IRepositoryScoreCalculator calculator) + { + _calculator = calculator ?? throw new ArgumentNullException(nameof(calculator)); + } + + public int Compare(IRepository x, IRepository y) + { + if (ReferenceEquals(x, y)) + { + return 0; + } + + if (ReferenceEquals(null, y)) + { + return 1; + } + + if (ReferenceEquals(null, x)) + { + return -1; + } + + var result = _calculator.Score(y) - _calculator.Score(x); + return result; + } +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/Score/ScoreComparerConfigurationV1.cs b/src/RepoM.Api/Ordering/Score/ScoreComparerConfigurationV1.cs new file mode 100644 index 00000000..ca29177f --- /dev/null +++ b/src/RepoM.Api/Ordering/Score/ScoreComparerConfigurationV1.cs @@ -0,0 +1,8 @@ +namespace RepoM.Api.Ordering.Score; + +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +public class ScoreComparerConfigurationV1 : IRepositoriesComparerConfiguration +{ + public IRepositoryScorerConfiguration? ScoreProvider { get; set; } +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/Score/ScoreComparerConfigurationV1Registration.cs b/src/RepoM.Api/Ordering/Score/ScoreComparerConfigurationV1Registration.cs new file mode 100644 index 00000000..f4562577 --- /dev/null +++ b/src/RepoM.Api/Ordering/Score/ScoreComparerConfigurationV1Registration.cs @@ -0,0 +1,11 @@ +namespace RepoM.Api.Ordering.Score; + +using System; +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +public class ScoreComparerConfigurationV1Registration : IConfigurationRegistration +{ + public Type ConfigurationType { get; } = typeof(ScoreComparerConfigurationV1); + + public string Tag => "score-comparer@1"; +} diff --git a/src/RepoM.Api/Ordering/Score/ScoreRepositoryComparerFactory.cs b/src/RepoM.Api/Ordering/Score/ScoreRepositoryComparerFactory.cs new file mode 100644 index 00000000..afaaf629 --- /dev/null +++ b/src/RepoM.Api/Ordering/Score/ScoreRepositoryComparerFactory.cs @@ -0,0 +1,19 @@ +namespace RepoM.Api.Ordering.Score; + +using System; +using RepoM.Core.Plugin.RepositoryOrdering; + +public class ScoreRepositoryComparerFactory : IRepositoryComparerFactory +{ + private readonly IRepositoryScoreCalculatorFactory _factory; + + public ScoreRepositoryComparerFactory(IRepositoryScoreCalculatorFactory factory) + { + _factory = factory ?? throw new ArgumentNullException(nameof(factory)); + } + + public IRepositoryComparer Create(ScoreComparerConfigurationV1 configuration) + { + return new ScoreComparer(_factory.Create(configuration.ScoreProvider!)); + } +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/Sum/SumComparerConfigurationV1.cs b/src/RepoM.Api/Ordering/Sum/SumComparerConfigurationV1.cs new file mode 100644 index 00000000..3b0911d2 --- /dev/null +++ b/src/RepoM.Api/Ordering/Sum/SumComparerConfigurationV1.cs @@ -0,0 +1,9 @@ +namespace RepoM.Api.Ordering.Sum; + +using System.Collections.Generic; +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +public class SumComparerConfigurationV1 : IRepositoriesComparerConfiguration +{ + public List Comparers { get; set; } = new(); +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/Sum/SumComparerConfigurationV1Registration.cs b/src/RepoM.Api/Ordering/Sum/SumComparerConfigurationV1Registration.cs new file mode 100644 index 00000000..ef962f84 --- /dev/null +++ b/src/RepoM.Api/Ordering/Sum/SumComparerConfigurationV1Registration.cs @@ -0,0 +1,11 @@ +namespace RepoM.Api.Ordering.Sum; + +using System; +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +public class SumComparerConfigurationV1Registration : IConfigurationRegistration +{ + public Type ConfigurationType { get; } = typeof(SumComparerConfigurationV1); + + public string Tag => "sum-comparer@1"; +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/Sum/SumCompositionComparer.cs b/src/RepoM.Api/Ordering/Sum/SumCompositionComparer.cs new file mode 100644 index 00000000..e1eeddfe --- /dev/null +++ b/src/RepoM.Api/Ordering/Sum/SumCompositionComparer.cs @@ -0,0 +1,36 @@ +namespace RepoM.Api.Ordering.Sum; + +using System.Collections.Generic; +using System.Linq; +using RepoM.Core.Plugin.Repository; +using RepoM.Core.Plugin.RepositoryOrdering; + +public class SumCompositionComparer : IRepositoryComparer +{ + private readonly IRepositoryComparer[] _comparers; + + public SumCompositionComparer(IEnumerable comparers) + { + _comparers = comparers.ToArray(); + } + + public int Compare(IRepository x, IRepository y) + { + if (ReferenceEquals(x, y)) + { + return 0; + } + + if (ReferenceEquals(null, y)) + { + return 1; + } + + if (ReferenceEquals(null, x)) + { + return -1; + } + + return _comparers.Sum(c => c.Compare(x, y)); + } +} \ No newline at end of file diff --git a/src/RepoM.Api/Ordering/Sum/SumRepositoryComparerFactory.cs b/src/RepoM.Api/Ordering/Sum/SumRepositoryComparerFactory.cs new file mode 100644 index 00000000..1aed0fe6 --- /dev/null +++ b/src/RepoM.Api/Ordering/Sum/SumRepositoryComparerFactory.cs @@ -0,0 +1,20 @@ +namespace RepoM.Api.Ordering.Sum; + +using System; +using System.Linq; +using RepoM.Core.Plugin.RepositoryOrdering; + +public class SumRepositoryComparerFactory : IRepositoryComparerFactory +{ + private readonly IRepositoryComparerFactory _factory; + + public SumRepositoryComparerFactory(IRepositoryComparerFactory factory) + { + _factory = factory ?? throw new ArgumentNullException(nameof(factory)); + } + + public IRepositoryComparer Create(SumComparerConfigurationV1 configuration) + { + return new SumCompositionComparer(configuration.Comparers.Select(c => _factory.Create(c))); + } +} \ No newline at end of file diff --git a/src/RepoM.Api/RepoM.Api.csproj b/src/RepoM.Api/RepoM.Api.csproj index ec7e36fe..610b798c 100644 --- a/src/RepoM.Api/RepoM.Api.csproj +++ b/src/RepoM.Api/RepoM.Api.csproj @@ -1,15 +1,22 @@  - netstandard2.0 + net6.0 + - + - + - + + + + + + + diff --git a/src/RepoM.Api/RepositoryActions/Decorators/LoggerActionExecutorDecorator.cs b/src/RepoM.Api/RepositoryActions/Decorators/LoggerActionExecutorDecorator.cs new file mode 100644 index 00000000..00c7efe3 --- /dev/null +++ b/src/RepoM.Api/RepositoryActions/Decorators/LoggerActionExecutorDecorator.cs @@ -0,0 +1,19 @@ +namespace RepoM.Api.RepositoryActions.Decorators; + +using RepoM.Core.Plugin.Repository; +using RepoM.Core.Plugin.RepositoryActions; + +public class LoggerActionExecutorDecorator : IActionExecutor where T : IAction +{ + private readonly IActionExecutor _decoratee; + + public LoggerActionExecutorDecorator(IActionExecutor decoratee) + { + _decoratee = decoratee; + } + + public void Execute(IRepository repository, T action) + { + _decoratee.Execute(repository, action); + } +} \ No newline at end of file diff --git a/src/RepoM.Api/RepositoryActions/Executors/BrowseActionExecutor.cs b/src/RepoM.Api/RepositoryActions/Executors/BrowseActionExecutor.cs new file mode 100644 index 00000000..f7a31326 --- /dev/null +++ b/src/RepoM.Api/RepositoryActions/Executors/BrowseActionExecutor.cs @@ -0,0 +1,16 @@ +namespace RepoM.Api.RepositoryActions.Executors; + +using System; +using JetBrains.Annotations; +using RepoM.Core.Plugin.Repository; +using RepoM.Core.Plugin.RepositoryActions; +using RepoM.Core.Plugin.RepositoryActions.Actions; + +[UsedImplicitly] +public class BrowseActionExecutor : IActionExecutor +{ + public void Execute(IRepository repository, BrowseAction action) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/RepoM.Api/RepositoryActions/Executors/DelegateActionExecutor.cs b/src/RepoM.Api/RepositoryActions/Executors/DelegateActionExecutor.cs new file mode 100644 index 00000000..39537fd0 --- /dev/null +++ b/src/RepoM.Api/RepositoryActions/Executors/DelegateActionExecutor.cs @@ -0,0 +1,15 @@ +namespace RepoM.Api.RepositoryActions.Executors; + +using JetBrains.Annotations; +using RepoM.Core.Plugin.Repository; +using RepoM.Core.Plugin.RepositoryActions; +using RepoM.Core.Plugin.RepositoryActions.Actions; + +[UsedImplicitly] +public class DelegateActionExecutor : IActionExecutor +{ + public void Execute(IRepository repository, DelegateAction action) + { + action.Action.Invoke(null, null); + } +} \ No newline at end of file diff --git a/src/RepoM.Api/RepositoryActions/Executors/NullActionExecutor.cs b/src/RepoM.Api/RepositoryActions/Executors/NullActionExecutor.cs new file mode 100644 index 00000000..0e4c39f8 --- /dev/null +++ b/src/RepoM.Api/RepositoryActions/Executors/NullActionExecutor.cs @@ -0,0 +1,15 @@ +namespace RepoM.Api.RepositoryActions.Executors; + +using JetBrains.Annotations; +using RepoM.Core.Plugin.Repository; +using RepoM.Core.Plugin.RepositoryActions; +using RepoM.Core.Plugin.RepositoryActions.Actions; + +[UsedImplicitly] +public class NullActionExecutor : IActionExecutor +{ + public void Execute(IRepository repository, NullAction action) + { + // intentionally do nothing. + } +} \ No newline at end of file diff --git a/src/RepoM.App/App.xaml.cs b/src/RepoM.App/App.xaml.cs index 1dc1dcd9..426c84df 100644 --- a/src/RepoM.App/App.xaml.cs +++ b/src/RepoM.App/App.xaml.cs @@ -35,6 +35,21 @@ namespace RepoM.App; using ILogger = Microsoft.Extensions.Logging.ILogger; using RepoM.App.Services; using RepoM.Api.IO.VariableProviders; +using RepoM.Api.Ordering.IsPinned; +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; +using RepoM.Core.Plugin.RepositoryOrdering; +using RepoM.Api.Ordering.Az; +using RepoM.Api.Ordering.Composition; +using RepoM.Api.Ordering.Label; +using RepoM.Api.Ordering.Score; +using RepoM.Api.Ordering.Sum; +using Container = SimpleInjector.Container; +using RepoM.Api.RepositoryActions.Decorators; +using RepoM.Core.Plugin.RepositoryActions; +using RepoM.Api.RepositoryActions.Executors; +using RepoM.App.RepositoryActions; +using RepoM.App.RepositoryOrdering; +using RepoM.Core.Plugin.Common; /// /// Interaction logic for App.xaml @@ -69,7 +84,6 @@ protected override void OnStartup(StartupEventArgs e) new FrameworkPropertyMetadata(System.Windows.Markup.XmlLanguage.GetLanguage(System.Globalization.CultureInfo.CurrentCulture.IetfLanguageTag))); Application.Current.Resources.MergedDictionaries[0] = ResourceDictionaryTranslationService.ResourceDictionary; - _notifyIcon = FindResource("NotifyIcon") as TaskbarIcon; var fileSystem = new FileSystem(); @@ -80,9 +94,12 @@ protected override void OnStartup(StartupEventArgs e) logger.LogInformation("Started"); RegisterLogging(loggerFactory); RegisterServices(_container, fileSystem); - UseRepositoryMonitor(_container); +#if DEBUG _container.Verify(VerificationOption.VerifyAndDiagnose); +#endif + + UseRepositoryMonitor(_container); _updateTimer = new Timer(async _ => await CheckForUpdatesAsync(), null, 5000, Timeout.Infinite); @@ -135,9 +152,9 @@ protected override void OnExit(ExitEventArgs e) _hotkey?.Unregister(); -#pragma warning disable CA1416 // Validate platform compatibility +// #pragma warning disable CA1416 // Validate platform compatibility _notifyIcon?.Dispose(); -#pragma warning restore CA1416 // Validate platform compatibility +// #pragma warning restore CA1416 // Validate platform compatibility base.OnExit(e); } @@ -208,12 +225,14 @@ private static void RegisterServices(Container container, IFileSystem fileSystem container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); + container.Register (Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); - + container.RegisterInstance(SystemClock.Instance); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); + container.Register(Lifestyle.Singleton); container.Collection.Append(Lifestyle.Singleton); container.RegisterInstance(fileSystem); @@ -224,7 +243,7 @@ private static void RegisterServices(Container container, IFileSystem fileSystem typeof(IVariableProvider).Assembly, typeof(RepositoryExpressionEvaluator).Assembly, }; - // container.Collection.Register(typeof(IVariableProvider), repoExpressionEvaluators, Lifestyle.Singleton); + container.Collection.Append(Lifestyle.Singleton); container.Collection.Append(Lifestyle.Singleton); container.Collection.Append(Lifestyle.Singleton); @@ -234,6 +253,7 @@ private static void RegisterServices(Container container, IFileSystem fileSystem container.Collection.Append(Lifestyle.Singleton); container.Collection.Append(Lifestyle.Singleton); container.Collection.Append(Lifestyle.Singleton); + container.Collection.Append(Lifestyle.Singleton); container.Collection.Register(typeof(IMethod), repoExpressionEvaluators, Lifestyle.Singleton); container.RegisterInstance(new DateTimeVariableProviderOptions @@ -263,6 +283,29 @@ private static void RegisterServices(Container container, IFileSystem fileSystem container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); + + container.RegisterSingleton(); + container.RegisterSingleton(); + + container.Collection.Register( + typeof(IConfigurationRegistration), + new[] { typeof(IConfigurationRegistration).Assembly, typeof(IsPinnedScorerConfigurationV1Registration).Assembly, }, + Lifestyle.Singleton); + + container.Register, IsPinnedScorerFactory>(Lifestyle.Singleton); + container.Register, TagScorerFactory>(Lifestyle.Singleton); + container.Register, AzRepositoryComparerFactory>(Lifestyle.Singleton); + container.Register, CompositionRepositoryComparerFactory>(Lifestyle.Singleton); + container.Register, ScoreRepositoryComparerFactory>(Lifestyle.Singleton); + container.Register, SumRepositoryComparerFactory>(Lifestyle.Singleton); + + container.RegisterSingleton(); + container.Register(typeof(IActionExecutor<>), new [] { typeof(BrowseActionExecutor).Assembly, }, Lifestyle.Singleton); + container.RegisterDecorator( + typeof(IActionExecutor<>), + typeof(LoggerActionExecutorDecorator<>), + Lifestyle.Singleton); + IEnumerable pluginDlls = PluginFinder.FindPluginAssemblies(Path.Combine(AppDomain.CurrentDomain.BaseDirectory), fileSystem); IEnumerable assemblies = pluginDlls.Select(plugin => Assembly.Load(AssemblyName.GetAssemblyName(plugin.FullName))); container.RegisterPackages(assemblies); diff --git a/src/RepoM.App/Converters/UtcToHumanizedLocalDateTimeConverter.cs b/src/RepoM.App/Converters/UtcToHumanizedLocalDateTimeConverter.cs index 1df7363d..a2c85c00 100644 --- a/src/RepoM.App/Converters/UtcToHumanizedLocalDateTimeConverter.cs +++ b/src/RepoM.App/Converters/UtcToHumanizedLocalDateTimeConverter.cs @@ -11,7 +11,7 @@ public class UtcToHumanizedLocalDateTimeConverter : IValueConverter public UtcToHumanizedLocalDateTimeConverter() { - _humanizer = new HardcodededMiniHumanizer(); + _humanizer = new HardcodededMiniHumanizer(SystemClock.Instance); } public object Convert(object value, Type targetType, object parameter, CultureInfo culture) diff --git a/src/RepoM.App/CustomRepositoryViewSortComparer.cs b/src/RepoM.App/CustomRepositoryViewSortComparer.cs deleted file mode 100644 index 426a6b2b..00000000 --- a/src/RepoM.App/CustomRepositoryViewSortComparer.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace RepoM.App; - -using System.Collections; -using RepoM.Api.Git; - -internal class CustomRepositoryViewSortComparer : IComparer -{ - public int Compare(object? x, object? y) - { - if (x is IRepositoryView xView && y is IRepositoryView yView) - { - return Compare(xView, yView); - } - - return 0; - } - - private static int Compare(IRepositoryView x, IRepositoryView y) - { - if (x.IsPinned == y.IsPinned) - { - return string.CompareOrdinal(x.Name, y.Name); - } - - // pinned should be first ;-) - return x.IsPinned ? -1 : 1; - } -} \ No newline at end of file diff --git a/src/RepoM.App/MainWindow.xaml b/src/RepoM.App/MainWindow.xaml index 8b3d805f..632a153c 100644 --- a/src/RepoM.App/MainWindow.xaml +++ b/src/RepoM.App/MainWindow.xaml @@ -8,6 +8,7 @@ xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:converters="clr-namespace:RepoM.App.Converters" xmlns:controls="clr-namespace:RepoM.App.Controls" + xmlns:app="clr-namespace:RepoM.App" TextElement.Foreground="{DynamicResource MaterialDesignBody}" TextElement.FontWeight="Regular" TextElement.FontSize="12" @@ -107,10 +108,15 @@ Grid.Column="1" Foreground="{DynamicResource MaterialDesignBody}" Style="{DynamicResource MaterialDesignFlatButton}"> - - - - + + + + + + @@ -119,10 +125,58 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/src/RepoM.App/MainWindow.xaml.cs b/src/RepoM.App/MainWindow.xaml.cs index 7e1cb686..416aacd7 100644 --- a/src/RepoM.App/MainWindow.xaml.cs +++ b/src/RepoM.App/MainWindow.xaml.cs @@ -16,9 +16,12 @@ namespace RepoM.App; using RepoM.Api; using RepoM.Api.Common; using RepoM.Api.Git; -using RepoM.Api.IO; using RepoM.App.Controls; +using RepoM.App.RepositoryActions; +using RepoM.App.RepositoryOrdering; using RepoM.App.Services; +using RepoM.Core.Plugin.Common; +using RepoM.Core.Plugin.RepositoryActions.Actions; using SourceChord.FluentWPF; /// @@ -35,6 +38,7 @@ public partial class MainWindow private bool _refreshDelayed; private DateTime _timeOfLastRefresh = DateTime.MinValue; private readonly IFileSystem _fileSystem; + private readonly ActionExecutor _executor; private readonly IAppDataPathProvider _appDataPathProvider; public MainWindow( @@ -47,14 +51,18 @@ public MainWindow( ITranslationService translationService, IAppDataPathProvider appDataPathProvider, IRepositorySearch repositorySearch, - IFileSystem fileSystem) + IFileSystem fileSystem, + ActionExecutor executor, + IRepositoryComparerManager repositoryComparerManager, + IThreadDispatcher threadDispatcher) { _translationService = translationService; InitializeComponent(); AcrylicWindow.SetAcrylicWindowStyle(this, AcrylicWindowStyle.None); - DataContext = new MainWindowPageModel(appSettingsService); + var orderingsViewModel = new OrderingsViewModel(repositoryComparerManager, threadDispatcher); + DataContext = new MainWindowPageModel(appSettingsService, orderingsViewModel); SettingsMenu.DataContext = DataContext; // this is out of the visual tree _monitor = repositoryMonitor as DefaultRepositoryMonitor; @@ -69,14 +77,16 @@ public MainWindow( _appDataPathProvider = appDataPathProvider ?? throw new ArgumentNullException(nameof(appDataPathProvider)); _repositorySearch = repositorySearch ?? throw new ArgumentNullException(nameof(repositorySearch)); _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _executor = executor ?? throw new ArgumentNullException(nameof(executor)); lstRepositories.ItemsSource = aggregator.Repositories; var view = (ListCollectionView)CollectionViewSource.GetDefaultView(aggregator.Repositories); ((ICollectionView)view).CollectionChanged += View_CollectionChanged; view.Filter = FilterRepositories; - view.CustomSort = new CustomRepositoryViewSortComparer(); - + view.CustomSort = repositoryComparerManager.Comparer; + repositoryComparerManager.SelectedRepositoryComparerKeyChanged += (_, _) => view.Refresh(); + AssemblyName? appName = Assembly.GetEntryAssembly()?.GetName(); txtHelpCaption.Text = appName?.Name + " " + appName?.Version?.ToString(2); txtHelp.Text = GetHelp(statusCharacterMap); @@ -87,7 +97,7 @@ public MainWindow( private void View_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { // use the list's itemsource directly, this one is not filtered (otherwise searching in the UI without matches could lead to the "no repositories yet"-screen) - var hasRepositories = lstRepositories.ItemsSource.OfType().Any(); + var hasRepositories = lstRepositories.ItemsSource.OfType().Any(); tbNoRepositories.Visibility = hasRepositories ? Visibility.Hidden : Visibility.Visible; } @@ -178,7 +188,7 @@ private void LstRepositories_ContextMenuOpening(object? sender, ContextMenuEvent private bool LstRepositoriesContextMenuOpening(object sender, ContextMenu ctxMenu) { - RepositoryView[] selectedViews = lstRepositories.SelectedItems.OfType().ToArray(); + RepositoryViewModel[] selectedViews = lstRepositories.SelectedItems.OfType().ToArray(); if (!selectedViews.Any()) { @@ -243,7 +253,7 @@ private void LstRepositories_KeyDown(object? sender, KeyEventArgs e) private void InvokeActionOnCurrentRepository() { - if (lstRepositories.SelectedItem is not RepositoryView selectedView) + if (lstRepositories.SelectedItem is not RepositoryViewModel selectedView) { return; } @@ -264,7 +274,10 @@ private void InvokeActionOnCurrentRepository() action = _repositoryActionProvider.GetPrimaryAction(selectedView.Repository); } - action?.Action?.Invoke(this, EventArgs.Empty); + if (action != null) + { + _executor.Execute(action.Repository, action.Action); + } } private void HelpButton_Click(object sender, RoutedEventArgs e) @@ -380,7 +393,7 @@ private void ShowUpdateIfAvailable() parent.ColumnDefinitions[Grid.GetColumn(UpdateButton)].Width = App.AvailableUpdate == null ? new GridLength(0) : GridLength.Auto; } - private static Control? /*MenuItem*/ CreateMenuItem(object sender, RepositoryActionBase action, IEnumerable? affectedViews = null) + private Control? /*MenuItem*/ CreateMenuItem(object sender, RepositoryActionBase action, IEnumerable? affectedViews = null) { if (action is RepositorySeparatorAction) { @@ -395,27 +408,27 @@ private void ShowUpdateIfAvailable() Action clickAction = (object clickSender, object clickArgs) => { - if (repositoryAction?.Action == null) + if (repositoryAction?.Action == null || repositoryAction.Action is NullAction) { return; } var coords = new float[] { 0, 0, }; - + // run actions in the UI async to not block it if (repositoryAction.ExecutionCausesSynchronizing) { Task.Run(() => SetViewsSynchronizing(affectedViews, true)) - .ContinueWith(t => repositoryAction.Action(null, coords)) + .ContinueWith(t => _executor.Execute(action.Repository, action.Action)) .ContinueWith(t => SetViewsSynchronizing(affectedViews, false)); } else { - Task.Run(() => repositoryAction.Action(null, coords)); + Task.Run(() => _executor.Execute(action.Repository, action.Action)); } }; - var item = new AcrylicMenuItem() + var item = new AcrylicMenuItem { Header = repositoryAction.Name, IsEnabled = repositoryAction.CanExecute, @@ -465,14 +478,14 @@ void SelfDetachingEventHandler(object _, RoutedEventArgs evtArgs) return item; } - private static void SetViewsSynchronizing(IEnumerable? affectedViews, bool synchronizing) + private static void SetViewsSynchronizing(IEnumerable? affectedViews, bool synchronizing) { if (affectedViews == null) { return; } - foreach (RepositoryView view in affectedViews) + foreach (RepositoryViewModel view in affectedViews) { view.IsSynchronizing = synchronizing; } @@ -557,7 +570,7 @@ private bool FilterRepositories(object item) return false; } - if (item is not RepositoryView viewModelItem) + if (item is not RepositoryViewModel viewModelItem) { return false; } diff --git a/src/RepoM.App/MainWindowPageModel.cs b/src/RepoM.App/MainWindowPageModel.cs index 3696f9ce..af380e20 100644 --- a/src/RepoM.App/MainWindowPageModel.cs +++ b/src/RepoM.App/MainWindowPageModel.cs @@ -1,21 +1,89 @@ namespace RepoM.App; using System; +using System.Collections.Generic; using System.ComponentModel; using System.Linq; using RepoM.Api.Common; using RepoM.Api.Git.AutoFetch; +using RepoM.App.RepositoryOrdering; +using RepoM.App.ViewModels; + +public class SortMenuItemViewModel : MenuItemViewModel +{ + private readonly Func _isSelectedFunc; + private readonly Action _setKeyFunc; + + public SortMenuItemViewModel( + Func isSelectedFunc, + Action setKeyFunc, + string title) + { + _isSelectedFunc = isSelectedFunc ?? throw new ArgumentNullException(nameof(isSelectedFunc)); + _setKeyFunc = setKeyFunc ?? throw new ArgumentNullException(nameof(setKeyFunc)); + + Header = title; + IsCheckable = true; + } + + public override bool IsChecked + { + get => _isSelectedFunc.Invoke(); + set => _setKeyFunc.Invoke(); + } + + public void Poke() + { + OnPropertyChanged(nameof(IsChecked)); + } +} + +public class OrderingsViewModel : List +{ + public OrderingsViewModel(IRepositoryComparerManager repositoryComparerManager, IThreadDispatcher threadDispatcher) + { + if (repositoryComparerManager == null) + { + throw new ArgumentNullException(nameof(repositoryComparerManager)); + } + + if (threadDispatcher == null) + { + throw new ArgumentNullException(nameof(threadDispatcher)); + } + + repositoryComparerManager.SelectedRepositoryComparerKeyChanged += (_, _) => + { + foreach (MenuItemViewModel item in this) + { + if (item is SortMenuItemViewModel sortMenuItemViewModel) + { + threadDispatcher.Invoke(() => sortMenuItemViewModel.Poke()); + } + } + }; + + AddRange(repositoryComparerManager.RepositoryComparerKeys.Select(name => + new SortMenuItemViewModel( + () => repositoryComparerManager.SelectedRepositoryComparerKey == name, + () => repositoryComparerManager.SetRepositoryComparer(name), + name))); + } +} public class MainWindowPageModel : INotifyPropertyChanged { private readonly IAppSettingsService _appSettingsService; public event PropertyChangedEventHandler? PropertyChanged; - public MainWindowPageModel(IAppSettingsService appSettingsService) + public MainWindowPageModel(IAppSettingsService appSettingsService, OrderingsViewModel orderingsViewModel) { _appSettingsService = appSettingsService ?? throw new ArgumentNullException(nameof(appSettingsService)); + Orderings = orderingsViewModel; } + public OrderingsViewModel Orderings { get; } + public AutoFetchMode AutoFetchMode { get => _appSettingsService.AutoFetchMode; diff --git a/src/RepoM.App/RepoM.App.csproj b/src/RepoM.App/RepoM.App.csproj index 6f62f75f..77e2fa9b 100644 --- a/src/RepoM.App/RepoM.App.csproj +++ b/src/RepoM.App/RepoM.App.csproj @@ -25,6 +25,7 @@ + @@ -36,21 +37,20 @@ - + - - + - + @@ -68,9 +68,5 @@ Settings.Designer.cs - - - - \ No newline at end of file diff --git a/src/RepoM.App/RepositoryActions/ActionExecutor.cs b/src/RepoM.App/RepositoryActions/ActionExecutor.cs new file mode 100644 index 00000000..14730655 --- /dev/null +++ b/src/RepoM.App/RepositoryActions/ActionExecutor.cs @@ -0,0 +1,22 @@ +namespace RepoM.App.RepositoryActions; + +using System; +using RepoM.Core.Plugin.Repository; +using RepoM.Core.Plugin.RepositoryActions; +using SimpleInjector; + +public sealed class ActionExecutor +{ + private readonly Container _container; + + public ActionExecutor(Container container) + { + _container = container ?? throw new ArgumentNullException(nameof(container)); + } + + public void Execute(IRepository repository, IAction action) + { + dynamic executor = _container.GetInstance(typeof(IActionExecutor<>).MakeGenericType(action.GetType())); + executor.Execute((dynamic)repository, (dynamic)action); + } +} \ No newline at end of file diff --git a/src/RepoM.App/RepositoryOrdering/ComparerComposition.cs b/src/RepoM.App/RepositoryOrdering/ComparerComposition.cs new file mode 100644 index 00000000..b76de040 --- /dev/null +++ b/src/RepoM.App/RepositoryOrdering/ComparerComposition.cs @@ -0,0 +1,34 @@ +namespace RepoM.App.RepositoryOrdering; + +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +internal class ComparerComposition : IComparer +{ + private readonly Dictionary _namedComparers; + private IComparer _selected; + + public ComparerComposition(Dictionary namedNamedComparers) + { + _namedComparers = namedNamedComparers; + _selected = _namedComparers.First().Value; + } + + public bool SetComparer(string key) + { + if (_namedComparers.TryGetValue(key, out IComparer? value)) + { + _selected = value; + return true; + } + + return false; + } + + public int Compare(object? x, object? y) + { + IComparer comparer = _selected; + return comparer.Compare(x, y); + } +} \ No newline at end of file diff --git a/src/RepoM.App/RepositoryOrdering/IRepositoryComparerManager.cs b/src/RepoM.App/RepositoryOrdering/IRepositoryComparerManager.cs new file mode 100644 index 00000000..0b9c0544 --- /dev/null +++ b/src/RepoM.App/RepositoryOrdering/IRepositoryComparerManager.cs @@ -0,0 +1,18 @@ +namespace RepoM.App.RepositoryOrdering; + +using System; +using System.Collections; +using System.Collections.Generic; + +public interface IRepositoryComparerManager +{ + event EventHandler? SelectedRepositoryComparerKeyChanged; + + IComparer Comparer { get; } + + IReadOnlyList RepositoryComparerKeys { get; } + + string SelectedRepositoryComparerKey { get; } + + bool SetRepositoryComparer(string key); +} \ No newline at end of file diff --git a/src/RepoM.App/RepositoryOrdering/RepositoryComparerAdapter.cs b/src/RepoM.App/RepositoryOrdering/RepositoryComparerAdapter.cs new file mode 100644 index 00000000..c1dbe802 --- /dev/null +++ b/src/RepoM.App/RepositoryOrdering/RepositoryComparerAdapter.cs @@ -0,0 +1,31 @@ +namespace RepoM.App.RepositoryOrdering; + +using System; +using System.Collections; +using RepoM.Api.Git; +using RepoM.Core.Plugin.RepositoryOrdering; + +internal class RepositoryComparerAdapter : IComparer +{ + private readonly IRepositoryComparer _comparer; + + public RepositoryComparerAdapter(IRepositoryComparer comparer) + { + _comparer = comparer ?? throw new ArgumentNullException(nameof(comparer)); + } + + public int Compare(object? x, object? y) + { + if (x is IRepositoryView xView && y is IRepositoryView yView) + { + return Compare(xView, yView); + } + + return 0; + } + + private int Compare(IRepositoryView x, IRepositoryView y) + { + return _comparer.Compare(x.Repository, y.Repository); + } +} \ No newline at end of file diff --git a/src/RepoM.App/RepositoryOrdering/RepositoryComparerCompositionFactory.cs b/src/RepoM.App/RepositoryOrdering/RepositoryComparerCompositionFactory.cs new file mode 100644 index 00000000..067c624c --- /dev/null +++ b/src/RepoM.App/RepositoryOrdering/RepositoryComparerCompositionFactory.cs @@ -0,0 +1,39 @@ +namespace RepoM.App.RepositoryOrdering; + +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; +using RepoM.Core.Plugin.RepositoryOrdering; +using System; +using Microsoft.Extensions.Logging; +using SimpleInjector; + +internal class RepositoryComparerCompositionFactory : IRepositoryComparerFactory +{ + private readonly Container _container; + private readonly ILogger _logger; + + public RepositoryComparerCompositionFactory(Container container, ILogger logger) + { + _container = container ?? throw new ArgumentNullException(nameof(container)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public IRepositoryComparer Create(IRepositoriesComparerConfiguration configuration) + { + try + { + return CreateInner(configuration); + } + catch (Exception e) + { + _logger.LogCritical(e, "Could not create a IRepositoryComparer for configuration type '{configuration}'", configuration); + throw; + } + } + + private IRepositoryComparer CreateInner(IRepositoriesComparerConfiguration configuration) + { + Type type = typeof(IRepositoryComparerFactory<>).MakeGenericType(configuration.GetType()); + dynamic factory = _container.GetInstance(type); + return factory.Create((dynamic)configuration); + } +} \ No newline at end of file diff --git a/src/RepoM.App/RepositoryOrdering/RepositoryComparerManager.cs b/src/RepoM.App/RepositoryOrdering/RepositoryComparerManager.cs new file mode 100644 index 00000000..7a7e9b37 --- /dev/null +++ b/src/RepoM.App/RepositoryOrdering/RepositoryComparerManager.cs @@ -0,0 +1,101 @@ +namespace RepoM.App.RepositoryOrdering; + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using RepoM.Api.Common; +using RepoM.Api.Ordering.Az; +using RepoM.Core.Plugin.RepositoryOrdering; +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +internal class RepositoryComparerManager : IRepositoryComparerManager +{ + private readonly IAppSettingsService _appSettingsService; + private readonly ILogger _logger; + private readonly ComparerComposition _comparer; + private readonly List _repositoryComparerKeys; + + public RepositoryComparerManager( + IAppSettingsService appSettingsService, + ICompareSettingsService compareSettingsService, + IRepositoryComparerFactory repositoryComparerFactory, + ILogger logger) + { + _appSettingsService = appSettingsService ?? throw new ArgumentNullException(nameof(appSettingsService)); + _ = compareSettingsService ?? throw new ArgumentNullException(nameof(compareSettingsService)); + _ = repositoryComparerFactory ?? throw new ArgumentNullException(nameof(repositoryComparerFactory)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + Dictionary multipleConfigurations = new (); + var comparers = new Dictionary(); + + try + { + multipleConfigurations = compareSettingsService.Configuration; + } + catch (Exception e) + { + _logger.LogError(e, "Could not get comparer configuration. Falling back to default. {message}", e.Message); + } + + foreach ((var key, IRepositoriesComparerConfiguration config) in multipleConfigurations) + { + try + { + if (!comparers.TryAdd(key, new RepositoryComparerAdapter(repositoryComparerFactory.Create(config)))) + { + _logger.LogWarning("Could not add comparer for key '{key}'.", key); + } + } + catch (Exception e) + { + _logger.LogError(e, "Could not create a repository comparer for key '{key}'. {message}", key, e.Message); + } + } + + if (comparers.Count == 0) + { + comparers.Add("Default", new RepositoryComparerAdapter(new AzComparer(1, "Name"))); + _logger.LogInformation("No custom comparers added, add default comparer"); + } + + _comparer = new ComparerComposition(comparers); + + _repositoryComparerKeys = comparers.Select(x => x.Key).ToList(); + + if (string.IsNullOrWhiteSpace(_appSettingsService.SortKey)) + { + _logger.LogInformation("Custom sorter key was not set. Pick first one."); + SetRepositoryComparer(_repositoryComparerKeys.First()); + } + else if (!SetRepositoryComparer(_appSettingsService.SortKey)) + { + _logger.LogInformation("Could not set comparer '{key}'. Falling back to first comparer.", _appSettingsService.SortKey); + SetRepositoryComparer(_repositoryComparerKeys.First()); + } + } + + public event EventHandler? SelectedRepositoryComparerKeyChanged; + + public IComparer Comparer => _comparer; + + public string SelectedRepositoryComparerKey { get; private set; } = "Default"; + + public IReadOnlyList RepositoryComparerKeys => _repositoryComparerKeys; + + public bool SetRepositoryComparer(string key) + { + if (!_comparer.SetComparer(key)) + { + _logger.LogWarning("Could not update/set the comparer key {key}.", key); + return false; + } + + _appSettingsService.SortKey = key; + SelectedRepositoryComparerKey = key; + SelectedRepositoryComparerKeyChanged?.Invoke(this, key); + return true; + } +} \ No newline at end of file diff --git a/src/RepoM.App/RepositoryOrdering/RepositoryScoreCalculatorFactory.cs b/src/RepoM.App/RepositoryOrdering/RepositoryScoreCalculatorFactory.cs new file mode 100644 index 00000000..5b5e8a89 --- /dev/null +++ b/src/RepoM.App/RepositoryOrdering/RepositoryScoreCalculatorFactory.cs @@ -0,0 +1,39 @@ +namespace RepoM.App.RepositoryOrdering; + +using System; +using Microsoft.Extensions.Logging; +using RepoM.Core.Plugin.RepositoryOrdering; +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; +using SimpleInjector; + +internal class RepositoryScoreCalculatorFactory : IRepositoryScoreCalculatorFactory +{ + private readonly Container _container; + private readonly ILogger _logger; + + public RepositoryScoreCalculatorFactory(Container container, ILogger logger) + { + _container = container ?? throw new ArgumentNullException(nameof(container)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public IRepositoryScoreCalculator Create(IRepositoryScorerConfiguration configuration) + { + try + { + return CreateInner(configuration); + } + catch (Exception e) + { + _logger.LogCritical(e, "Could not create a IRepositoryComparer for configuration type '{configuration}'", configuration); + throw; + } + } + + private IRepositoryScoreCalculator CreateInner(IRepositoryScorerConfiguration configuration) + { + Type type = typeof(IRepositoryScoreCalculatorFactory<>).MakeGenericType(configuration.GetType()); + dynamic factory = _container.GetInstance(type); + return factory.Create((dynamic)configuration); + } +} \ No newline at end of file diff --git a/src/RepoM.App/ViewModels/MenuItemViewModel.cs b/src/RepoM.App/ViewModels/MenuItemViewModel.cs new file mode 100644 index 00000000..8700cad4 --- /dev/null +++ b/src/RepoM.App/ViewModels/MenuItemViewModel.cs @@ -0,0 +1,36 @@ +namespace RepoM.App.ViewModels; + +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +// https://stackoverflow.com/questions/5912687/styling-contextmenu-and-contextmenu-items +// https://learn.microsoft.com/en-us/dotnet/desktop/wpf/controls/contextmenu-styles-and-templates?view=netframeworkdesktop-4.8 +// https://itecnote.com/tecnote/wpf-how-to-bind-an-observablecollection-of-viewmodels-to-a-menuitem/ +public class MenuItemViewModel : INotifyPropertyChanged +{ + public string Header { get; set; } = string.Empty; + + public bool IsCheckable { get; set; } + + public virtual bool IsChecked { get; set; } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + protected bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) + { + return false; + } + + field = value; + OnPropertyChanged(propertyName); + return true; + } +} \ No newline at end of file diff --git a/src/RepoM.Api/IO/IAppDataPathProvider.cs b/src/RepoM.Core.Plugin/Common/IAppDataPathProvider.cs similarity index 81% rename from src/RepoM.Api/IO/IAppDataPathProvider.cs rename to src/RepoM.Core.Plugin/Common/IAppDataPathProvider.cs index eb2df89d..7e79e77a 100644 --- a/src/RepoM.Api/IO/IAppDataPathProvider.cs +++ b/src/RepoM.Core.Plugin/Common/IAppDataPathProvider.cs @@ -1,4 +1,4 @@ -namespace RepoM.Api.IO; +namespace RepoM.Core.Plugin.Common; using System; diff --git a/src/RepoM.Api/Common/IClock.cs b/src/RepoM.Core.Plugin/Common/IClock.cs similarity index 65% rename from src/RepoM.Api/Common/IClock.cs rename to src/RepoM.Core.Plugin/Common/IClock.cs index f03a40bc..361beb73 100644 --- a/src/RepoM.Api/Common/IClock.cs +++ b/src/RepoM.Core.Plugin/Common/IClock.cs @@ -1,4 +1,4 @@ -namespace RepoM.Api.Common; +namespace RepoM.Core.Plugin.Common; using System; diff --git a/src/RepoM.Core.Plugin/RepoM.Core.Plugin.csproj b/src/RepoM.Core.Plugin/RepoM.Core.Plugin.csproj index 21feb56d..dd45f053 100644 --- a/src/RepoM.Core.Plugin/RepoM.Core.Plugin.csproj +++ b/src/RepoM.Core.Plugin/RepoM.Core.Plugin.csproj @@ -1,11 +1,13 @@ - + - netstandard2.0 + net6.0 + + diff --git a/src/RepoM.Core.Plugin/Repository/IRepository.cs b/src/RepoM.Core.Plugin/Repository/IRepository.cs new file mode 100644 index 00000000..431d4402 --- /dev/null +++ b/src/RepoM.Core.Plugin/Repository/IRepository.cs @@ -0,0 +1,24 @@ +namespace RepoM.Core.Plugin.Repository; + +using System.Collections.Generic; + +public interface IRepository +{ + string Name { get; } + + string Path { get; } + + string Location { get; } + + string CurrentBranch { get; } + + string[] Branches { get; } + + string[] LocalBranches { get; } + + string[] Tags { get; } + + string SafePath { get; } + + List Remotes { get; } +} \ No newline at end of file diff --git a/src/RepoM.Api/Git/Remote.cs b/src/RepoM.Core.Plugin/Repository/Remote.cs similarity index 94% rename from src/RepoM.Api/Git/Remote.cs rename to src/RepoM.Core.Plugin/Repository/Remote.cs index c10ddaf9..191a986b 100644 --- a/src/RepoM.Api/Git/Remote.cs +++ b/src/RepoM.Core.Plugin/Repository/Remote.cs @@ -1,4 +1,4 @@ -namespace RepoM.Api.Git; +namespace RepoM.Core.Plugin.Repository; using System; using System.Diagnostics; @@ -16,7 +16,7 @@ public Remote(string key, string url) public string Key { get; } - public string Name { get; set; } + public string Name { get; } public string Url { get; } @@ -39,7 +39,7 @@ private static string CalculateName(string url) { return name; } - + try { var parts = url.Split('/', '\\'); diff --git a/src/RepoM.Core.Plugin/Repository/RepositoryContext.cs b/src/RepoM.Core.Plugin/Repository/RepositoryContext.cs new file mode 100644 index 00000000..e8a1ebfd --- /dev/null +++ b/src/RepoM.Core.Plugin/Repository/RepositoryContext.cs @@ -0,0 +1,25 @@ +namespace RepoM.Core.Plugin.Repository; + +using System; +using System.Collections.Generic; +using System.Linq; + +public sealed class RepositoryContext +{ + public RepositoryContext() + { + Repositories = Array.Empty(); + } + + public RepositoryContext(params IRepository[] repositories) + { + Repositories = repositories.ToArray(); + } + + public RepositoryContext(IEnumerable repositories) + { + Repositories = repositories.ToArray(); + } + + public IRepository[] Repositories { get; } +} \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/RepositoryActions/Actions/BrowseAction.cs b/src/RepoM.Core.Plugin/RepositoryActions/Actions/BrowseAction.cs new file mode 100644 index 00000000..48920f0c --- /dev/null +++ b/src/RepoM.Core.Plugin/RepositoryActions/Actions/BrowseAction.cs @@ -0,0 +1,7 @@ +namespace RepoM.Core.Plugin.RepositoryActions.Actions; + +using RepoM.Core.Plugin.RepositoryActions; + +public sealed class BrowseAction : IAction +{ +} \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/RepositoryActions/Actions/DelegateAction.cs b/src/RepoM.Core.Plugin/RepositoryActions/Actions/DelegateAction.cs new file mode 100644 index 00000000..0f9985ab --- /dev/null +++ b/src/RepoM.Core.Plugin/RepositoryActions/Actions/DelegateAction.cs @@ -0,0 +1,14 @@ +namespace RepoM.Core.Plugin.RepositoryActions.Actions; + +using System; +using RepoM.Core.Plugin.RepositoryActions; + +public sealed class DelegateAction : IAction +{ + public DelegateAction(Action action) + { + Action = action ?? throw new ArgumentNullException(nameof(action)); + } + + public Action Action { get; } +} \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/RepositoryActions/Actions/NullAction.cs b/src/RepoM.Core.Plugin/RepositoryActions/Actions/NullAction.cs new file mode 100644 index 00000000..019519f3 --- /dev/null +++ b/src/RepoM.Core.Plugin/RepositoryActions/Actions/NullAction.cs @@ -0,0 +1,12 @@ +namespace RepoM.Core.Plugin.RepositoryActions.Actions; + +using RepoM.Core.Plugin.RepositoryActions; + +public sealed class NullAction : IAction +{ + private NullAction() + { + } + + public static NullAction Instance { get; } = new NullAction(); +} \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/RepositoryActions/IAction.cs b/src/RepoM.Core.Plugin/RepositoryActions/IAction.cs new file mode 100644 index 00000000..7e853a54 --- /dev/null +++ b/src/RepoM.Core.Plugin/RepositoryActions/IAction.cs @@ -0,0 +1,5 @@ +namespace RepoM.Core.Plugin.RepositoryActions; + +public interface IAction +{ +} \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/RepositoryActions/IActionExecutor.cs b/src/RepoM.Core.Plugin/RepositoryActions/IActionExecutor.cs new file mode 100644 index 00000000..17848262 --- /dev/null +++ b/src/RepoM.Core.Plugin/RepositoryActions/IActionExecutor.cs @@ -0,0 +1,8 @@ +namespace RepoM.Core.Plugin.RepositoryActions; + +using RepoM.Core.Plugin.Repository; + +public interface IActionExecutor where T : IAction +{ + void Execute(IRepository repository, T action); +} \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/RepositoryOrdering/Configuration/IConfigurationRegistration.cs b/src/RepoM.Core.Plugin/RepositoryOrdering/Configuration/IConfigurationRegistration.cs new file mode 100644 index 00000000..93ec4a4b --- /dev/null +++ b/src/RepoM.Core.Plugin/RepositoryOrdering/Configuration/IConfigurationRegistration.cs @@ -0,0 +1,13 @@ +namespace RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +using System; + +/// +/// Configuration registration per name +/// +public interface IConfigurationRegistration +{ + public Type ConfigurationType { get; } + + public string Tag { get; } +} \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/RepositoryOrdering/Configuration/IRepositoriesComparerConfiguration.cs b/src/RepoM.Core.Plugin/RepositoryOrdering/Configuration/IRepositoriesComparerConfiguration.cs new file mode 100644 index 00000000..c1cddcba --- /dev/null +++ b/src/RepoM.Core.Plugin/RepositoryOrdering/Configuration/IRepositoriesComparerConfiguration.cs @@ -0,0 +1,8 @@ +namespace RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +/// +/// Configuration to compare two repos +/// +public interface IRepositoriesComparerConfiguration +{ +} \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/RepositoryOrdering/Configuration/IRepositoryScorerConfiguration.cs b/src/RepoM.Core.Plugin/RepositoryOrdering/Configuration/IRepositoryScorerConfiguration.cs new file mode 100644 index 00000000..117fb1ad --- /dev/null +++ b/src/RepoM.Core.Plugin/RepositoryOrdering/Configuration/IRepositoryScorerConfiguration.cs @@ -0,0 +1,8 @@ +namespace RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +/// +/// Configuration to score a single repo +/// +public interface IRepositoryScorerConfiguration +{ +} \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/RepositoryOrdering/IRepositoryComparer.cs b/src/RepoM.Core.Plugin/RepositoryOrdering/IRepositoryComparer.cs new file mode 100644 index 00000000..05c90de3 --- /dev/null +++ b/src/RepoM.Core.Plugin/RepositoryOrdering/IRepositoryComparer.cs @@ -0,0 +1,8 @@ +namespace RepoM.Core.Plugin.RepositoryOrdering; + +using System.Collections.Generic; +using RepoM.Core.Plugin.Repository; + +public interface IRepositoryComparer : IComparer +{ +} \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/RepositoryOrdering/IRepositoryComparerFactory.cs b/src/RepoM.Core.Plugin/RepositoryOrdering/IRepositoryComparerFactory.cs new file mode 100644 index 00000000..ac5c5143 --- /dev/null +++ b/src/RepoM.Core.Plugin/RepositoryOrdering/IRepositoryComparerFactory.cs @@ -0,0 +1,14 @@ +namespace RepoM.Core.Plugin.RepositoryOrdering; + +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +public interface IRepositoryComparerFactory +{ + IRepositoryComparer Create(IRepositoriesComparerConfiguration configuration); +} + +public interface IRepositoryComparerFactory + where TConfig : IRepositoriesComparerConfiguration +{ + IRepositoryComparer Create(TConfig configuration); +} \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/RepositoryOrdering/IRepositoryScoreCalculator.cs b/src/RepoM.Core.Plugin/RepositoryOrdering/IRepositoryScoreCalculator.cs new file mode 100644 index 00000000..a3107606 --- /dev/null +++ b/src/RepoM.Core.Plugin/RepositoryOrdering/IRepositoryScoreCalculator.cs @@ -0,0 +1,8 @@ +namespace RepoM.Core.Plugin.RepositoryOrdering; + +using RepoM.Core.Plugin.Repository; + +public interface IRepositoryScoreCalculator +{ + int Score(IRepository repository); +} \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/RepositoryOrdering/IRepositoryScoreCalculatorFactory.cs b/src/RepoM.Core.Plugin/RepositoryOrdering/IRepositoryScoreCalculatorFactory.cs new file mode 100644 index 00000000..10d6343d --- /dev/null +++ b/src/RepoM.Core.Plugin/RepositoryOrdering/IRepositoryScoreCalculatorFactory.cs @@ -0,0 +1,14 @@ +namespace RepoM.Core.Plugin.RepositoryOrdering; + +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +public interface IRepositoryScoreCalculatorFactory +{ + IRepositoryScoreCalculator Create(IRepositoryScorerConfiguration configuration); +} + +public interface IRepositoryScoreCalculatorFactory + where TConfig : IRepositoryScorerConfiguration +{ + IRepositoryScoreCalculator Create(TConfig config); +} \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/SimpleInjector/IPackage.cs b/src/RepoM.Core.Plugin/SimpleInjector/IPackage.cs new file mode 100644 index 00000000..d37e9a7d --- /dev/null +++ b/src/RepoM.Core.Plugin/SimpleInjector/IPackage.cs @@ -0,0 +1,36 @@ +// Copyright (c) Simple Injector Contributors. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for license information. + +namespace SimpleInjector.Packaging +{ + using System.Collections.Generic; + using System.Reflection; + + /// + /// Contract for types allow registering a set of services. + /// + /// + /// The following example shows an implementation of an . + /// (); + /// container.Register(); + /// } + /// } + /// ]]> + /// The following example shows how to load all defined packages, using the + /// RegisterPackages method. + /// + /// + public interface IPackage + { + /// Registers the set of services in the specified . + /// The container the set of services is registered into. + void RegisterServices(Container container); + } +} \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/SimpleInjector/PackageExtensions.cs b/src/RepoM.Core.Plugin/SimpleInjector/PackageExtensions.cs new file mode 100644 index 00000000..2e741a84 --- /dev/null +++ b/src/RepoM.Core.Plugin/SimpleInjector/PackageExtensions.cs @@ -0,0 +1,147 @@ +// Copyright (c) Simple Injector Contributors. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for license information. + +// This class is placed in the root namespace to allow users to start using these extension methods after +// adding the assembly reference, without find and add the correct namespace. +namespace SimpleInjector +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Globalization; + using System.Linq; + using System.Reflection; + using SimpleInjector.Packaging; + + /// + /// Extension methods for working with packages. + /// + public static class PackageExtensions + { + /// + /// Loads all implementations from the given set of + /// and calls their Register method. + /// Note that only publicly exposed classes that contain a public default constructor will be loaded. + /// + /// The container to which the packages will be applied to. + /// The assemblies that will be searched for packages. + /// Thrown when the is a null + /// reference. + public static void RegisterPackages(this Container container, IEnumerable assemblies) + { + if (container is null) + { + throw new ArgumentNullException(nameof(container)); + } + + if (assemblies is null) + { + throw new ArgumentNullException(nameof(assemblies)); + } + + foreach (var package in container.GetPackagesToRegister(assemblies)) + { + package.RegisterServices(container); + } + } + + /// + /// Loads all implementations from the given set of + /// and returns a list of created package instances. + /// + /// The container. + /// The assemblies that will be searched for packages. + /// Returns a list of created packages. + public static IPackage[] GetPackagesToRegister( + this Container container, IEnumerable assemblies) + { + if (container is null) + { + throw new ArgumentNullException(nameof(container)); + } + + if (assemblies is null) + { + throw new ArgumentNullException(nameof(assemblies)); + } + + assemblies = assemblies.ToArray(); + + if (assemblies.Any(a => a is null)) + { + throw new ArgumentNullException( + "The elements of the supplied collection should not be null.", + nameof(assemblies)); + } + + var packageTypes = ( + from assembly in assemblies + from type in GetExportedTypesFrom(assembly) + where typeof(IPackage).Info().IsAssignableFrom(type.Info()) + where !type.Info().IsAbstract + where !type.Info().IsGenericTypeDefinition + select type) + .ToArray(); + + RequiresPackageTypesHaveDefaultConstructor(packageTypes); + + return packageTypes.Select(CreatePackage).ToArray(); + } + + private static IEnumerable GetExportedTypesFrom(Assembly assembly) + { + try + { + return assembly.DefinedTypes.Select(info => info.AsType()); + } + catch (NotSupportedException) + { + // A type load exception would typically happen on an Anonymously Hosted DynamicMethods + // Assembly and it would be safe to skip this exception. + return Enumerable.Empty(); + } + } + + private static void RequiresPackageTypesHaveDefaultConstructor(Type[] packageTypes) + { + var invalidPackageType = + packageTypes.FirstOrDefault(type => !type.HasDefaultConstructor()); + + if (invalidPackageType != null) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + "The type {0} does not contain a default (public parameterless) constructor. " + + "Packages must have a default constructor.", + invalidPackageType.FullName)); + } + } + + private static IPackage CreatePackage(Type packageType) + { + try + { + return (IPackage)Activator.CreateInstance(packageType); + } + catch (Exception ex) + { + string message = string.Format( + CultureInfo.InvariantCulture, + "The creation of package type {0} failed. {1}", + packageType.FullName, + ex.Message); + + throw new InvalidOperationException(message, ex); + } + } + + private static bool HasDefaultConstructor(this Type type) => + type.GetConstructors().Any(ctor => !ctor.GetParameters().Any()); + + private static ConstructorInfo[] GetConstructors(this Type type) => + type.GetTypeInfo().DeclaredConstructors.ToArray(); + + private static TypeInfo Info(this Type type) => type.GetTypeInfo(); + } +} \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/VariableProviders/IVariableProvider.cs b/src/RepoM.Core.Plugin/VariableProviders/IVariableProvider.cs new file mode 100644 index 00000000..9726cb02 --- /dev/null +++ b/src/RepoM.Core.Plugin/VariableProviders/IVariableProvider.cs @@ -0,0 +1,22 @@ +namespace RepoM.Core.Plugin.VariableProviders; + +/// +/// IVariableProvider. +/// +public interface IVariableProvider +{ + /// + /// CanProvide. + /// + /// key. + /// bool. + bool CanProvide(string key); + + /// + /// Provide. + /// + /// key. + /// arguments. + /// variable value. + object? Provide(string key, string? arg); +} \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/VariableProviders/IVariableProvider{T}.cs b/src/RepoM.Core.Plugin/VariableProviders/IVariableProvider{T}.cs new file mode 100644 index 00000000..3e522b0b --- /dev/null +++ b/src/RepoM.Core.Plugin/VariableProviders/IVariableProvider{T}.cs @@ -0,0 +1,17 @@ +namespace RepoM.Core.Plugin.VariableProviders; + +/// +/// Typed IVariableProvider. +/// +/// Context type. +public interface IVariableProvider : IVariableProvider +{ + /// + /// Provide. + /// + /// The context. + /// key. + /// arguments. + /// variable value. + object? Provide(T context, string key, string? arg); +} \ No newline at end of file diff --git a/src/RepoM.Plugin.AzureDevOps/ActionAzureDevOpsPullRequestsV1Mapper.cs b/src/RepoM.Plugin.AzureDevOps/ActionAzureDevOpsPullRequestsV1Mapper.cs index 0e55d471..cb13c614 100644 --- a/src/RepoM.Plugin.AzureDevOps/ActionAzureDevOpsPullRequestsV1Mapper.cs +++ b/src/RepoM.Plugin.AzureDevOps/ActionAzureDevOpsPullRequestsV1Mapper.cs @@ -11,6 +11,7 @@ namespace RepoM.Plugin.AzureDevOps; using RepoM.Api.IO.ExpressionEvaluator; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.ActionMappers; +using RepoM.Core.Plugin.RepositoryActions.Actions; using RepositoryAction = RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data.RepositoryAction; [UsedImplicitly] @@ -91,7 +92,7 @@ private Api.Git.RepositoryAction[] Map(RepositoryActionAzureDevOpsPullRequestsV1 } catch (Exception e) { - var notificationItem = new Api.Git.RepositoryAction($"An error occurred grabbing pull requests. {e.Message}") + var notificationItem = new Api.Git.RepositoryAction($"An error occurred grabbing pull requests. {e.Message}", repository) { CanExecute = false, ExecutionCausesSynchronizing = false, @@ -102,13 +103,13 @@ private Api.Git.RepositoryAction[] Map(RepositoryActionAzureDevOpsPullRequestsV1 if (pullRequests.Any()) { var results = new List(pullRequests.Count); - results.AddRange(pullRequests.Select(pr => new Api.Git.RepositoryAction(pr.Name) + results.AddRange(pullRequests.Select(pr => new Api.Git.RepositoryAction(pr.Name, repository) { - Action = (_, _) => + Action = new DelegateAction((_, _) => { _logger.LogInformation("PullRequest {Url}", pr.Url); ProcessHelper.StartProcess(pr.Url, string.Empty); - }, + }), })); return results.ToArray(); @@ -118,7 +119,7 @@ private Api.Git.RepositoryAction[] Map(RepositoryActionAzureDevOpsPullRequestsV1 // check if user wants a notification if (_expressionEvaluator.EvaluateBooleanExpression(action.ShowWhenEmpty, repository)) { - var notificationItem = new Api.Git.RepositoryAction("No PRs found.") + var notificationItem = new Api.Git.RepositoryAction("No PRs found.", repository) { CanExecute = false, ExecutionCausesSynchronizing = false, diff --git a/src/RepoM.Plugin.AzureDevOps/AzureDevOpsPackage.cs b/src/RepoM.Plugin.AzureDevOps/AzureDevOpsPackage.cs index aa48fafc..71a91a30 100644 --- a/src/RepoM.Plugin.AzureDevOps/AzureDevOpsPackage.cs +++ b/src/RepoM.Plugin.AzureDevOps/AzureDevOpsPackage.cs @@ -1,5 +1,6 @@ namespace RepoM.Plugin.AzureDevOps; +using System; using JetBrains.Annotations; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider; using RepoM.Core.Plugin; diff --git a/src/RepoM.Plugin.AzureDevOps/RepoM.Plugin.AzureDevOps.csproj b/src/RepoM.Plugin.AzureDevOps/RepoM.Plugin.AzureDevOps.csproj index 4c377057..06954750 100644 --- a/src/RepoM.Plugin.AzureDevOps/RepoM.Plugin.AzureDevOps.csproj +++ b/src/RepoM.Plugin.AzureDevOps/RepoM.Plugin.AzureDevOps.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/RepoM.Plugin.EverythingFileSearch/EverythingGitRepositoryFinderFactory.cs b/src/RepoM.Plugin.EverythingFileSearch/EverythingGitRepositoryFinderFactory.cs index e25ac7e6..1b446c00 100644 --- a/src/RepoM.Plugin.EverythingFileSearch/EverythingGitRepositoryFinderFactory.cs +++ b/src/RepoM.Plugin.EverythingFileSearch/EverythingGitRepositoryFinderFactory.cs @@ -17,7 +17,7 @@ public EverythingGitRepositoryFinderFactory(IPathSkipper pathSkipper) _pathSkipper = pathSkipper ?? throw new ArgumentNullException(nameof(pathSkipper)); } - public string Name { get; } = "Everything"; + public string Name => "Everything"; public bool IsActive => _isInstalled.Value; diff --git a/src/RepoM.Plugin.EverythingFileSearch/RepoM.Plugin.EverythingFileSearch.csproj b/src/RepoM.Plugin.EverythingFileSearch/RepoM.Plugin.EverythingFileSearch.csproj index 83162336..65148b3d 100644 --- a/src/RepoM.Plugin.EverythingFileSearch/RepoM.Plugin.EverythingFileSearch.csproj +++ b/src/RepoM.Plugin.EverythingFileSearch/RepoM.Plugin.EverythingFileSearch.csproj @@ -6,8 +6,6 @@ - - diff --git a/src/RepoM.Plugin.LuceneSearch/RepoM.Plugin.LuceneSearch.csproj b/src/RepoM.Plugin.LuceneSearch/RepoM.Plugin.LuceneSearch.csproj index b97792b9..66895739 100644 --- a/src/RepoM.Plugin.LuceneSearch/RepoM.Plugin.LuceneSearch.csproj +++ b/src/RepoM.Plugin.LuceneSearch/RepoM.Plugin.LuceneSearch.csproj @@ -16,8 +16,6 @@ - - diff --git a/src/RepoM.Plugin.SonarCloud/ActionSonarCloudV1Mapper.cs b/src/RepoM.Plugin.SonarCloud/ActionSonarCloudV1Mapper.cs index 26a1b350..57f8349b 100644 --- a/src/RepoM.Plugin.SonarCloud/ActionSonarCloudV1Mapper.cs +++ b/src/RepoM.Plugin.SonarCloud/ActionSonarCloudV1Mapper.cs @@ -10,6 +10,7 @@ namespace RepoM.Plugin.SonarCloud; using RepoM.Api.IO.ExpressionEvaluator; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.ActionMappers; +using RepoM.Core.Plugin.RepositoryActions.Actions; using RepositoryAction = RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data.RepositoryAction; [UsedImplicitly] @@ -61,20 +62,30 @@ IEnumerable IActionToRepositoryActionMapper.Map(Repository var name = NameHelper.EvaluateName(action.Name, repository, _translationService, _expressionEvaluator); var key = _expressionEvaluator.EvaluateStringExpression(action.Project!, repository); - yield return new Api.Git.RepositoryAction(name) - { - Action = (_, _) => - { - try - { - _ = _service.SetFavorite(key); - } - catch (Exception) + if (_service.IsInitialized) + { + yield return new Api.Git.RepositoryAction(name, repository) + { + Action = new DelegateAction((_, _) => { - // ignore - } - }, - ExecutionCausesSynchronizing = false, - }; + try + { + _ = _service.SetFavorite(key); + } + catch (Exception) + { + // ignore + } + }), + ExecutionCausesSynchronizing = false, + }; + } + else + { + yield return new Api.Git.RepositoryAction(name, repository) + { + CanExecute = false, + }; + } } } \ No newline at end of file diff --git a/src/RepoM.Plugin.SonarCloud/RepoM.Plugin.SonarCloud.csproj b/src/RepoM.Plugin.SonarCloud/RepoM.Plugin.SonarCloud.csproj index c6041191..49196294 100644 --- a/src/RepoM.Plugin.SonarCloud/RepoM.Plugin.SonarCloud.csproj +++ b/src/RepoM.Plugin.SonarCloud/RepoM.Plugin.SonarCloud.csproj @@ -7,8 +7,6 @@ - - diff --git a/src/RepoM.Plugin.SonarCloud/SonarCloudFavoriteService.cs b/src/RepoM.Plugin.SonarCloud/SonarCloudFavoriteService.cs index 5afb3025..4cbabf32 100644 --- a/src/RepoM.Plugin.SonarCloud/SonarCloudFavoriteService.cs +++ b/src/RepoM.Plugin.SonarCloud/SonarCloudFavoriteService.cs @@ -45,6 +45,8 @@ public Task InitializeAsync() return Task.CompletedTask; } + public bool IsInitialized => _client != null; + public async Task SetFavorite(string repoKey) { SonarQubeClient? c = _client; diff --git a/src/RepoM.Plugin.Statistics/IReadOnlyRepositoryStatistics.cs b/src/RepoM.Plugin.Statistics/IReadOnlyRepositoryStatistics.cs new file mode 100644 index 00000000..bca28754 --- /dev/null +++ b/src/RepoM.Plugin.Statistics/IReadOnlyRepositoryStatistics.cs @@ -0,0 +1,10 @@ +namespace RepoM.Plugin.Statistics; + +using System; + +internal interface IReadOnlyRepositoryStatistics +{ + int GetRecordingCount(DateTime from, DateTime to); + int GetRecordingCountFrom(DateTime from); + int GetRecordingCountBefore(DateTime to); +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/Interface/IEvent.cs b/src/RepoM.Plugin.Statistics/Interface/IEvent.cs new file mode 100644 index 00000000..9ece58c3 --- /dev/null +++ b/src/RepoM.Plugin.Statistics/Interface/IEvent.cs @@ -0,0 +1,10 @@ +namespace RepoM.Plugin.Statistics.Interface; + +using System; + +public interface IEvent +{ + public string Repository { get; set; } + + public DateTime Timestamp { get; set; } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/Interface/RepositoryActionRecordedEvent.cs b/src/RepoM.Plugin.Statistics/Interface/RepositoryActionRecordedEvent.cs new file mode 100644 index 00000000..c180c96c --- /dev/null +++ b/src/RepoM.Plugin.Statistics/Interface/RepositoryActionRecordedEvent.cs @@ -0,0 +1,10 @@ +namespace RepoM.Plugin.Statistics.Interface; + +using System; + +internal class RepositoryActionRecordedEvent : IEvent +{ + public string Repository { get; set; } = null!; + + public DateTime Timestamp { get; set; } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/Ordering/LastOpenedComparer.cs b/src/RepoM.Plugin.Statistics/Ordering/LastOpenedComparer.cs new file mode 100644 index 00000000..cd5d9dde --- /dev/null +++ b/src/RepoM.Plugin.Statistics/Ordering/LastOpenedComparer.cs @@ -0,0 +1,66 @@ +namespace RepoM.Plugin.Statistics.Ordering; + +using System; +using System.Collections.Generic; +using System.Linq; +using RepoM.Core.Plugin.Repository; +using RepoM.Core.Plugin.RepositoryOrdering; + +internal class LastOpenedComparer : IRepositoryComparer +{ + private readonly StatisticsService _service; + private readonly int _weight; + + public LastOpenedComparer(StatisticsService service, int weight) + { + _service = service ?? throw new ArgumentNullException(nameof(service)); + _weight = weight; + } + + public int Compare(IRepository? x, IRepository? y) + { + if (_weight == 0) + { + return 0; + } + + if (ReferenceEquals(x, y)) + { + return 0; + } + + if (ReferenceEquals(null, y)) + { + return _weight; + } + + if (ReferenceEquals(null, x)) + { + return -1 * _weight; + } + + DateTime lastX = GetLast(x); + DateTime lastY = GetLast(y); + + if (lastX == lastY) + { + return 0; + } + + if (lastX < lastY) + { + return _weight; + } + + return -1 * _weight; + } + + private DateTime GetLast(IRepository repository) + { + IReadOnlyList items = _service.GetRecordings(repository); + + return items.Count == 0 + ? DateTime.MinValue + : items.MaxBy(x => x); + } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/Ordering/LastOpenedComparerFactory.cs b/src/RepoM.Plugin.Statistics/Ordering/LastOpenedComparerFactory.cs new file mode 100644 index 00000000..fa4d63b0 --- /dev/null +++ b/src/RepoM.Plugin.Statistics/Ordering/LastOpenedComparerFactory.cs @@ -0,0 +1,19 @@ +namespace RepoM.Plugin.Statistics.Ordering; + +using System; +using RepoM.Core.Plugin.RepositoryOrdering; + +public sealed class LastOpenedComparerFactory : IRepositoryComparerFactory +{ + private readonly StatisticsService _service; + + public LastOpenedComparerFactory(StatisticsService service) + { + _service = service ?? throw new ArgumentNullException(nameof(service)); + } + + public IRepositoryComparer Create(LastOpenedConfigurationV1 configuration) + { + return new LastOpenedComparer(_service, configuration.Weight); + } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/Ordering/LastOpenedConfigurationV1.cs b/src/RepoM.Plugin.Statistics/Ordering/LastOpenedConfigurationV1.cs new file mode 100644 index 00000000..d36d6915 --- /dev/null +++ b/src/RepoM.Plugin.Statistics/Ordering/LastOpenedConfigurationV1.cs @@ -0,0 +1,8 @@ +namespace RepoM.Plugin.Statistics.Ordering; + +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +public sealed class LastOpenedConfigurationV1 : IRepositoriesComparerConfiguration +{ + public int Weight { get; set; } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/Ordering/LastOpenedConfigurationV1Registration.cs b/src/RepoM.Plugin.Statistics/Ordering/LastOpenedConfigurationV1Registration.cs new file mode 100644 index 00000000..9197ab09 --- /dev/null +++ b/src/RepoM.Plugin.Statistics/Ordering/LastOpenedConfigurationV1Registration.cs @@ -0,0 +1,13 @@ +namespace RepoM.Plugin.Statistics.Ordering; + +using System; +using JetBrains.Annotations; +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +[UsedImplicitly] +public class LastOpenedConfigurationV1Registration : IConfigurationRegistration +{ + public Type ConfigurationType { get; } = typeof(LastOpenedConfigurationV1); + + public string Tag => "last-opened-comparer@1"; +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/Ordering/ScoreCalculatorConfig.cs b/src/RepoM.Plugin.Statistics/Ordering/ScoreCalculatorConfig.cs new file mode 100644 index 00000000..ef820e82 --- /dev/null +++ b/src/RepoM.Plugin.Statistics/Ordering/ScoreCalculatorConfig.cs @@ -0,0 +1,10 @@ +namespace RepoM.Plugin.Statistics.Ordering; + +using System.Collections.Generic; + +internal class ScoreCalculatorConfig +{ + public List Ranges { get; set; } = new(); + + public int MaxScore { get; set; } = int.MaxValue; +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/Ordering/ScoreCalculatorRangeConfig.cs b/src/RepoM.Plugin.Statistics/Ordering/ScoreCalculatorRangeConfig.cs new file mode 100644 index 00000000..bf60631c --- /dev/null +++ b/src/RepoM.Plugin.Statistics/Ordering/ScoreCalculatorRangeConfig.cs @@ -0,0 +1,12 @@ +namespace RepoM.Plugin.Statistics.Ordering; + +using System; + +internal class ScoreCalculatorRangeConfig +{ + public TimeSpan MaxAge { get; set; } + + public int Score { get; set;} + + public int MaxItems { get; set; } = int.MaxValue; +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/Ordering/UsageScoreCalculator.cs b/src/RepoM.Plugin.Statistics/Ordering/UsageScoreCalculator.cs new file mode 100644 index 00000000..fe18fe2b --- /dev/null +++ b/src/RepoM.Plugin.Statistics/Ordering/UsageScoreCalculator.cs @@ -0,0 +1,106 @@ +namespace RepoM.Plugin.Statistics.Ordering; + +using System; +using System.Collections.Generic; +using System.Linq; +using RepoM.Core.Plugin.Common; +using RepoM.Core.Plugin.Repository; +using RepoM.Core.Plugin.RepositoryOrdering; + +internal class UsageScoreCalculator : IRepositoryScoreCalculator +{ + private readonly StatisticsService _service; + private readonly IClock _clock; + private readonly ScoreCalculatorConfig _config; + private readonly List _ranges; + + public UsageScoreCalculator(StatisticsService service, IClock clock, ScoreCalculatorConfig config) + { + _service = service ?? throw new ArgumentNullException(nameof(service)); + _clock = clock ?? throw new ArgumentNullException(nameof(clock)); + _config = config ?? throw new ArgumentNullException(nameof(config)); + _ranges = config.Ranges.OrderBy(x => x.MaxAge).ToList(); + } + + public int Score(IRepository repository) + { + if (_ranges.Count == 0) + { + return 0; + } + + if (_config.MaxScore == 0) + { + return 0; + } + + DateTime now = _clock.Now; + + IReadOnlyRepositoryStatistics? repositoryRecording = _service.GetRepositoryRecording(repository); + if (repositoryRecording == null) + { + return 0; + } + + var score = CalculateScore(now, repositoryRecording); + + if (score < 0) + { + return 0; + } + + if (score > _config.MaxScore) + { + return _config.MaxScore; + } + + return score; + } + + private int CalculateScore(DateTime now, IReadOnlyRepositoryStatistics repositoryRecording) + { + var score = 0; + var unused = 0; + var currentCount = 0; + + ScoreCalculatorRangeConfig previousRange = _ranges[0]; + ScoreCalculatorRangeConfig currentRange = _ranges[0]; + + DateTime dateTime = now.Subtract(currentRange.MaxAge); + + currentCount = repositoryRecording.GetRecordingCountFrom(dateTime); + if (currentCount <= currentRange.MaxItems) + { + score += currentCount * currentRange.Score; + unused = 0; + } + else + { + score += currentRange.MaxItems * currentRange.Score; + unused = currentCount - currentRange.MaxItems; + } + + for (var i = 1; i < _ranges.Count; i++) + { + currentRange = _ranges[i]; + currentCount = repositoryRecording.GetRecordingCount( + now.Subtract(currentRange.MaxAge), + now.Subtract(previousRange.MaxAge)) + unused; + + if (currentCount <= currentRange.MaxItems) + { + score += currentCount * currentRange.Score; + unused = 0; + } + else + { + score += currentRange.MaxItems * currentRange.Score; + unused = currentCount - currentRange.MaxItems; + } + + previousRange = currentRange; + } + + return score; + } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/Ordering/UsageScorerConfigurationV1.cs b/src/RepoM.Plugin.Statistics/Ordering/UsageScorerConfigurationV1.cs new file mode 100644 index 00000000..92cedc4b --- /dev/null +++ b/src/RepoM.Plugin.Statistics/Ordering/UsageScorerConfigurationV1.cs @@ -0,0 +1,21 @@ +namespace RepoM.Plugin.Statistics.Ordering; + +using System; +using System.Collections.Generic; +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +public sealed class UsageScorerConfigurationV1 : IRepositoryScorerConfiguration +{ + public List Windows { get; set; } = new List(); + + public int? MaxScore { get; set; } = null; +} + +public sealed class Windows +{ + public TimeSpan Until { get; set; } + + public int Weight { get; set; } + + public int MaxItems { get; set; } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/Ordering/UsageScorerConfigurationV1Registration.cs b/src/RepoM.Plugin.Statistics/Ordering/UsageScorerConfigurationV1Registration.cs new file mode 100644 index 00000000..44d35178 --- /dev/null +++ b/src/RepoM.Plugin.Statistics/Ordering/UsageScorerConfigurationV1Registration.cs @@ -0,0 +1,13 @@ +namespace RepoM.Plugin.Statistics.Ordering; + +using System; +using JetBrains.Annotations; +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +[UsedImplicitly] +public class UsageScorerConfigurationV1Registration : IConfigurationRegistration +{ + public Type ConfigurationType { get; } = typeof(UsageScorerConfigurationV1); + + public string Tag => "usage-scorer@1"; +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/Ordering/UsageScorerFactory.cs b/src/RepoM.Plugin.Statistics/Ordering/UsageScorerFactory.cs new file mode 100644 index 00000000..0ce68224 --- /dev/null +++ b/src/RepoM.Plugin.Statistics/Ordering/UsageScorerFactory.cs @@ -0,0 +1,41 @@ +namespace RepoM.Plugin.Statistics.Ordering; + +using System; +using System.Linq; +using Microsoft.Extensions.Logging; +using RepoM.Core.Plugin.Common; +using RepoM.Core.Plugin.RepositoryOrdering; + +public sealed class UsageScorerFactory : IRepositoryScoreCalculatorFactory +{ + private readonly StatisticsService _service; + private readonly IClock _clock; + + public UsageScorerFactory(StatisticsService service, IClock clock) + { + _service = service ?? throw new ArgumentNullException(nameof(service)); + _clock = clock ?? throw new ArgumentNullException(nameof(clock)); + } + + public IRepositoryScoreCalculator Create(UsageScorerConfigurationV1 config) + { + var scoreCalculatorConfig = new ScoreCalculatorConfig + { + MaxScore = config.MaxScore ?? int.MaxValue, + Ranges = config.Windows + .Select(x => new ScoreCalculatorRangeConfig + { + Score = x.Weight, + MaxItems = x.MaxItems, + MaxAge = x.Until, + }) + .ToList(), + }; + + + return new UsageScoreCalculator( + _service, + _clock, + scoreCalculatorConfig); + } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/RepoM.Plugin.Statistics.csproj b/src/RepoM.Plugin.Statistics/RepoM.Plugin.Statistics.csproj new file mode 100644 index 00000000..1cefe858 --- /dev/null +++ b/src/RepoM.Plugin.Statistics/RepoM.Plugin.Statistics.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + + + + + + + + + + + + + + diff --git a/src/RepoM.Plugin.Statistics/RepositoryActions/RecordStatisticsActionExecutorDecorator.cs b/src/RepoM.Plugin.Statistics/RepositoryActions/RecordStatisticsActionExecutorDecorator.cs new file mode 100644 index 00000000..ec3bbaaa --- /dev/null +++ b/src/RepoM.Plugin.Statistics/RepositoryActions/RecordStatisticsActionExecutorDecorator.cs @@ -0,0 +1,44 @@ +namespace RepoM.Plugin.Statistics.RepositoryActions; + +using System; +using JetBrains.Annotations; +using RepoM.Core.Plugin.Repository; +using RepoM.Core.Plugin.RepositoryActions; +using RepoM.Core.Plugin.RepositoryActions.Actions; + +[UsedImplicitly] +public sealed class RecordStatisticsActionExecutorDecorator : IActionExecutor where T : IAction +{ + private readonly IActionExecutor _decoratee; + private readonly StatisticsService _service; + + public RecordStatisticsActionExecutorDecorator( + IActionExecutor decoratee, + StatisticsService service) + { + _decoratee = decoratee ?? throw new ArgumentNullException(nameof(decoratee)); + _service = service ?? throw new ArgumentNullException(nameof(service)); + } + + public void Execute(IRepository repository, T action) + { + if (action.GetType() != typeof(NullAction)) + { + Record(repository); + } + + _decoratee.Execute(repository, action); + } + + private void Record(IRepository repository) + { + try + { + _service.Record(repository); + } + catch (Exception) + { + // swallow + } + } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/RepositoryStatistics.cs b/src/RepoM.Plugin.Statistics/RepositoryStatistics.cs new file mode 100644 index 00000000..f59a17d8 --- /dev/null +++ b/src/RepoM.Plugin.Statistics/RepositoryStatistics.cs @@ -0,0 +1,65 @@ +namespace RepoM.Plugin.Statistics; + +using System; +using System.Collections.Generic; +using System.Linq; +using RepoM.Core.Plugin.Common; +using RepoM.Plugin.Statistics.Interface; + +internal class RepositoryStatistics : IReadOnlyRepositoryStatistics +{ + private readonly string _repositoryPath; + private readonly IClock _clock; + + public RepositoryStatistics(string repositoryPath, IClock clock) + { + _repositoryPath = repositoryPath ?? throw new ArgumentNullException(nameof(repositoryPath)); + _clock = clock ?? throw new ArgumentNullException(nameof(clock)); + } + + public List Recordings { get; } = new(); + + public IEvent Record() + { + var evt = new RepositoryActionRecordedEvent + { + Repository = _repositoryPath, + Timestamp = _clock.Now, + }; + + Apply(evt); + + return evt; + } + + int IReadOnlyRepositoryStatistics.GetRecordingCount(DateTime from, DateTime to) + { + return Recordings.Count(recordingDate => recordingDate < to && recordingDate >= from); + } + + int IReadOnlyRepositoryStatistics.GetRecordingCountFrom(DateTime from) + { + return Recordings.Count(recordingDate => recordingDate >= from); + } + + int IReadOnlyRepositoryStatistics.GetRecordingCountBefore(DateTime to) + { + return Recordings.Count(recordingDate => recordingDate < to); + } + + public void Apply(IEvent evt) + { + if (evt is RepositoryActionRecordedEvent repositoryActionRecordedEvent) + { + Apply(repositoryActionRecordedEvent); + return; + } + + throw new NotImplementedException(); + } + + private void Apply(RepositoryActionRecordedEvent evt) + { + Recordings.Add(evt.Timestamp); + } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/StatisticsModule.cs b/src/RepoM.Plugin.Statistics/StatisticsModule.cs new file mode 100644 index 00000000..e95b8a9b --- /dev/null +++ b/src/RepoM.Plugin.Statistics/StatisticsModule.cs @@ -0,0 +1,180 @@ +namespace RepoM.Plugin.Statistics; + +using System; +using System.IO.Abstractions; +using System.Linq; +using System.Reactive.Concurrency; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using RepoM.Core.Plugin; +using RepoM.Core.Plugin.Common; +using RepoM.Plugin.Statistics.Interface; + +[UsedImplicitly] +internal class StatisticsModule : IModule +{ + private readonly StatisticsService _service; + private readonly IClock _clock; + private readonly IAppDataPathProvider _pathProvider; + private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; + private string _basePath = string.Empty; + private IDisposable? _disposable; + private readonly JsonSerializerSettings _settings; + + public StatisticsModule( + StatisticsService service, + IClock clock, + IAppDataPathProvider pathProvider, + IFileSystem fileSystem, + ILogger logger) + { + _service = service ?? throw new ArgumentNullException(nameof(service)); + _clock = clock ?? throw new ArgumentNullException(nameof(clock)); + _pathProvider = pathProvider ?? throw new ArgumentNullException(nameof(pathProvider)); + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _settings = new JsonSerializerSettings + { + Formatting = Formatting.None, + NullValueHandling = NullValueHandling.Ignore, + TypeNameHandling = TypeNameHandling.All, + }; + } + + public async Task StartAsync() + { + _basePath = _fileSystem.Path.Combine(_pathProvider.GetAppDataPath(), "Module", "Statistics"); + + _disposable = WriteEventsToFile(); + + await ProcessEventsFromFile().ConfigureAwait(false); + + _ = Task.Run(RemoveOldFilesAsync); + } + + public Task StopAsync() + { + _disposable?.Dispose(); + return Task.CompletedTask; + } + + private async Task RemoveOldFilesAsync() + { + if (!_fileSystem.Directory.Exists(_basePath)) + { + return; + } + + IOrderedEnumerable orderedEnumerable = _fileSystem.Directory.GetFiles(_basePath, "statistics.v1.*.json").OrderBy(f => f); + + DateTime threshold = _clock.Now.AddDays(-30); + + foreach (var file in orderedEnumerable) + { + IEvent[] list = Array.Empty(); + + try + { + var json = await _fileSystem.File.ReadAllTextAsync(file, CancellationToken.None).ConfigureAwait(false); + list = JsonConvert.DeserializeObject(json, _settings) ?? Array.Empty(); + } + catch (Exception e) + { + _logger.LogError(e, "Could not read or deserialize data from '{filename}'. {message}", file, e.Message); + } + + if (list.All(item => item.Timestamp <= threshold)) + { + try + { + _logger.LogDebug("Remove old Statistics file '{filename}'", file); + _fileSystem.File.Delete(file); + } + catch (Exception e) + { + _logger.LogError(e, "Could not delete '{filename}'. {message}", file, e.Message); + } + } + } + } + + private async Task ProcessEventsFromFile() + { + if (!_fileSystem.Directory.Exists(_basePath)) + { + return; + } + + IOrderedEnumerable orderedEnumerable = _fileSystem.Directory.GetFiles(_basePath, "statistics.v1.*.json").OrderBy(f => f); + + foreach (var file in orderedEnumerable) + { + IEvent[] list = Array.Empty(); + + try + { + var json = await _fileSystem.File.ReadAllTextAsync(file, CancellationToken.None).ConfigureAwait(false); + list = JsonConvert.DeserializeObject(json, _settings) ?? Array.Empty(); + } + catch (Exception e) + { + _logger.LogError(e, "Could not read or deserialize data from '{filename}'. {message}", file, e.Message); + } + + foreach (IEvent item in list) + { + _service.Apply(item); + } + } + } + + private IDisposable WriteEventsToFile() + { + return _service + .Events + .ObserveOn(Scheduler.Default) + .Buffer(TimeSpan.FromMinutes(5)) + .Subscribe(data => + { + IEvent[] events = data.ToArray(); + if (events.Length == 0) + { + return; + } + + var json = JsonConvert.SerializeObject(events, _settings); + var filename = _fileSystem.Path.Combine(_basePath, $"statistics.v1.{_clock.Now:yyyy-MM-dd HH.mm.ss}.json"); + + if (!_fileSystem.Directory.Exists(_basePath)) + { + try + { + _logger.LogDebug("Try Create directory '{basePath}'.", _basePath); + _fileSystem.Directory.CreateDirectory(_basePath); + } + catch (Exception e) + { + _logger.LogError(e, "Could not create directory '{basePath}'. {message}", _basePath, e.Message); + } + } + + if (_fileSystem.Directory.Exists(_basePath)) + { + try + { + _fileSystem.File.WriteAllText(filename, json); + } + catch (Exception e) + { + _logger.LogError(e, "Could not write json to '{filename}'. {message}", filename, e.Message); + } + } + }); + } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/StatisticsPackage.cs b/src/RepoM.Plugin.Statistics/StatisticsPackage.cs new file mode 100644 index 00000000..26ec41f0 --- /dev/null +++ b/src/RepoM.Plugin.Statistics/StatisticsPackage.cs @@ -0,0 +1,47 @@ +namespace RepoM.Plugin.Statistics; + +using JetBrains.Annotations; +using RepoM.Core.Plugin; +using RepoM.Core.Plugin.RepositoryActions; +using RepoM.Core.Plugin.RepositoryOrdering; +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; +using RepoM.Core.Plugin.VariableProviders; +using RepoM.Plugin.Statistics.Ordering; +using RepoM.Plugin.Statistics.RepositoryActions; +using RepoM.Plugin.Statistics.VariableProviders; +using SimpleInjector; +using SimpleInjector.Packaging; + +[UsedImplicitly] +public class StatisticsPackage : IPackage +{ + public void RegisterServices(Container container) + { + RegisterPluginHooks(container); + RegisterInternals(container); + } + + private static void RegisterPluginHooks(Container container) + { + // ordering + container.Collection.Append(Lifestyle.Singleton); + container.Register, UsageScorerFactory>(Lifestyle.Singleton); + + container.Collection.Append(Lifestyle.Singleton); + container.Register, LastOpenedComparerFactory>(Lifestyle.Singleton); + + // action executor + container.RegisterDecorator(typeof(IActionExecutor<>), typeof(RecordStatisticsActionExecutorDecorator<>), Lifestyle.Singleton); + + // variable provider + container.Collection.Append(Lifestyle.Singleton); + + // module + container.Collection.Append(Lifestyle.Singleton); + } + + private static void RegisterInternals(Container container) + { + container.Register(Lifestyle.Singleton); + } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/StatisticsService.cs b/src/RepoM.Plugin.Statistics/StatisticsService.cs new file mode 100644 index 00000000..437f3a9a --- /dev/null +++ b/src/RepoM.Plugin.Statistics/StatisticsService.cs @@ -0,0 +1,73 @@ +namespace RepoM.Plugin.Statistics; + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Subjects; +using RepoM.Core.Plugin.Common; +using RepoM.Core.Plugin.Repository; +using RepoM.Plugin.Statistics.Interface; + +public class StatisticsService +{ + private readonly IClock _clock; + private readonly ReadOnlyCollection _empty = new List(0).AsReadOnly(); + private readonly ConcurrentDictionary _recordings = new(); + private readonly Subject _events; + + public StatisticsService(IClock clock) + { + _clock = clock ?? throw new ArgumentNullException(nameof(clock)); + _events = new Subject(); + } + + public IObservable Events => _events; + + public void Record(IRepository repository) + { + if (!_recordings.TryGetValue(repository.SafePath, out RepositoryStatistics? repositoryStatistics)) + { + repositoryStatistics = new RepositoryStatistics(repository.SafePath, _clock); + _recordings.TryAdd(repository.SafePath, repositoryStatistics); + } + + IEvent evt = repositoryStatistics.Record(); + _events.OnNext(evt); + } + + public IReadOnlyList GetRepositories() + { + return _recordings.Select(x => x.Key).ToImmutableArray(); + } + + internal IReadOnlyRepositoryStatistics? GetRepositoryRecording(IRepository repository) + { + return _recordings.TryGetValue(repository.SafePath, out RepositoryStatistics? repositoryStatistics) + ? repositoryStatistics + : null; + } + + public IReadOnlyList GetRecordings(IRepository repository) + { + if (_recordings.TryGetValue(repository.SafePath, out RepositoryStatistics? repositoryStatistics)) + { + return repositoryStatistics.Recordings.AsReadOnly(); + } + + return _empty; + } + + public void Apply(IEvent evt) + { + if (!_recordings.TryGetValue(evt.Repository, out RepositoryStatistics? repositoryStatistics)) + { + repositoryStatistics = new RepositoryStatistics(evt.Repository, _clock); + _recordings.TryAdd(evt.Repository, repositoryStatistics); + } + + repositoryStatistics.Apply(evt); + } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/VariableProviders/UsageVariableProvider.cs b/src/RepoM.Plugin.Statistics/VariableProviders/UsageVariableProvider.cs new file mode 100644 index 00000000..c9703524 --- /dev/null +++ b/src/RepoM.Plugin.Statistics/VariableProviders/UsageVariableProvider.cs @@ -0,0 +1,33 @@ +namespace RepoM.Plugin.Statistics.VariableProviders; + +using System; +using System.Linq; +using JetBrains.Annotations; +using RepoM.Core.Plugin.Repository; +using RepoM.Core.Plugin.VariableProviders; + +[UsedImplicitly] +public class UsageVariableProvider : IVariableProvider +{ + private readonly StatisticsService _service; + + public UsageVariableProvider(StatisticsService service) + { + _service = service ?? throw new ArgumentNullException(nameof(service)); + } + + public bool CanProvide(string key) + { + return !string.IsNullOrWhiteSpace(key) && key.Equals("usage", StringComparison.CurrentCultureIgnoreCase); + } + + public object? Provide(RepositoryContext context, string key, string? arg) + { + return _service.GetRecordings(context.Repositories.First()).Count; + } + + public object? Provide(string key, string? arg) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.WindowsExplorerGitInfo/RepoM.Plugin.WindowsExplorerGitInfo.csproj b/src/RepoM.Plugin.WindowsExplorerGitInfo/RepoM.Plugin.WindowsExplorerGitInfo.csproj index 4e5189bc..7eb86a9e 100644 --- a/src/RepoM.Plugin.WindowsExplorerGitInfo/RepoM.Plugin.WindowsExplorerGitInfo.csproj +++ b/src/RepoM.Plugin.WindowsExplorerGitInfo/RepoM.Plugin.WindowsExplorerGitInfo.csproj @@ -7,8 +7,6 @@ - - diff --git a/tests/RepoM.Api.Tests/DynamicRepositoryActionDeserializerFactory.cs b/tests/RepoM.Api.Tests/DynamicRepositoryActionDeserializerFactory.cs index 04f45043..2f13da13 100644 --- a/tests/RepoM.Api.Tests/DynamicRepositoryActionDeserializerFactory.cs +++ b/tests/RepoM.Api.Tests/DynamicRepositoryActionDeserializerFactory.cs @@ -26,6 +26,7 @@ public static JsonDynamicRepositoryActionDeserializer Create() new ActionAssociateFileV1Deserializer(), new ActionPinRepositoryV1Deserializer(), new ActionForEachV1Deserializer(), + new ActionJustTextV1Deserializer(), })); } diff --git a/tests/RepoM.Api.Tests/IO/DefaultRepositoryActionProviderTest.cs b/tests/RepoM.Api.Tests/IO/DefaultRepositoryActionProviderTest.cs index 68265d9d..b1963b2b 100644 --- a/tests/RepoM.Api.Tests/IO/DefaultRepositoryActionProviderTest.cs +++ b/tests/RepoM.Api.Tests/IO/DefaultRepositoryActionProviderTest.cs @@ -15,8 +15,8 @@ namespace RepoM.Api.Common.Tests.IO; using FakeItEasy; using RepoM.Api.Common; using RepoM.Api.Git; -using RepoM.Api.IO; using RepoM.Api.IO.VariableProviders; +using RepoM.Core.Plugin.Common; using VerifyXunit; [UsesEasyTestFile] diff --git a/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Action/JustTextV1Test.cs b/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Action/JustTextV1Test.cs new file mode 100644 index 00000000..643fc152 --- /dev/null +++ b/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Action/JustTextV1Test.cs @@ -0,0 +1,61 @@ +namespace RepoM.Api.Tests.IO.ModuleBasedRepositoryActionProvider.Action; + +using System.Threading.Tasks; +using EasyTestFile; +using EasyTestFileXunit; +using FluentAssertions; +using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.ActionDeserializers; +using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Data.Actions; +using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Deserialization; +using VerifyTests; +using VerifyXunit; +using Xunit; + +[UsesEasyTestFile] +[UsesVerify] +public class JustTextV1Test +{ + private readonly JsonDynamicRepositoryActionDeserializer _sut; + private readonly EasyTestFileSettings _testFileSettings; + private readonly VerifySettings _verifySettings; + + public JustTextV1Test() + { + _sut = DynamicRepositoryActionDeserializerFactory.CreateWithDeserializer(new ActionJustTextV1Deserializer()); + + _testFileSettings = new EasyTestFileSettings(); + _testFileSettings.UseDirectory("TestFiles"); + _testFileSettings.UseExtension("json"); + + _verifySettings = new VerifySettings(); + _verifySettings.UseDirectory("Verified"); + } + + [Fact] + public async Task Deserialize_JustText1() + { + // arrange + var content = await EasyTestFile.LoadAsText(_testFileSettings); + + // act + var result = _sut.Deserialize(content); + + // assert + await Verifier.Verify(result, _verifySettings); + } + + + [Fact] + public async Task Deserialize_ShouldBeOfExpectedType() + { + // arrange + _testFileSettings.UseMethodName(nameof(Deserialize_JustText1)); + var content = await EasyTestFile.LoadAsText(_testFileSettings); + + // act + var result = _sut.Deserialize(content); + + // assert + _ = result.ActionsCollection.Actions.Should().AllBeOfType(); + } +} \ No newline at end of file diff --git a/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Action/TestFiles/JustTextV1Test.Deserialize_JustText1.testfile.json b/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Action/TestFiles/JustTextV1Test.Deserialize_JustText1.testfile.json new file mode 100644 index 00000000..8788cf19 --- /dev/null +++ b/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Action/TestFiles/JustTextV1Test.Deserialize_JustText1.testfile.json @@ -0,0 +1,30 @@ +{ + "repository-actions": { + "actions": [ + { + "type": "just-text@1", + "name": "This is a message" + }, + { + "type": "just-text@1", + "name": "This is a message 2", + "enabled": true + }, + { + "type": "just-text@1", + "name": "This is a message 3", + "enabled": false + }, + { + "type": "just-text@1", + "name": "This is a message 4", + "enabled": "false" + }, + { + "type": "just-text@1", + "name": "This is a message 5", + "enabled": "dummy" + } + ] + } +} \ No newline at end of file diff --git a/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Action/Verified/JustTextV1Test.Deserialize_JustText1.verified.txt b/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Action/Verified/JustTextV1Test.Deserialize_JustText1.verified.txt new file mode 100644 index 00000000..3d41364f --- /dev/null +++ b/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Action/Verified/JustTextV1Test.Deserialize_JustText1.verified.txt @@ -0,0 +1,36 @@ +{ + TagsCollection: {}, + ActionsCollection: { + Actions: [ + { + $type: RepositoryActionJustTextV1, + Type: just-text@1, + Name: This is a message + }, + { + $type: RepositoryActionJustTextV1, + Enabled: true, + Type: just-text@1, + Name: This is a message 2 + }, + { + $type: RepositoryActionJustTextV1, + Enabled: false, + Type: just-text@1, + Name: This is a message 3 + }, + { + $type: RepositoryActionJustTextV1, + Enabled: false, + Type: just-text@1, + Name: This is a message 4 + }, + { + $type: RepositoryActionJustTextV1, + Enabled: dummy, + Type: just-text@1, + Name: This is a message 5 + } + ] + } +} \ No newline at end of file diff --git a/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/ActionMapperCompositionFactory.cs b/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/ActionMapperCompositionFactory.cs index 7ed1fb1c..89712ae0 100644 --- a/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/ActionMapperCompositionFactory.cs +++ b/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/ActionMapperCompositionFactory.cs @@ -32,6 +32,7 @@ public static ActionMapperComposition Create( new ActionIgnoreRepositoriesV1Mapper(expressionEvaluator, translationService, repositoryMonitor), new ActionSeparatorV1Mapper(expressionEvaluator), new ActionAssociateFileV1Mapper(expressionEvaluator, translationService), + new ActionJustTextV1Mapper(expressionEvaluator, translationService), }; return new ActionMapperComposition(list, expressionEvaluator); diff --git a/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/RepositorySpecificConfigurationTest.cs b/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/RepositorySpecificConfigurationTest.cs index 37c02425..d9b4dc37 100644 --- a/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/RepositorySpecificConfigurationTest.cs +++ b/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/RepositorySpecificConfigurationTest.cs @@ -21,12 +21,12 @@ namespace RepoM.Api.Tests.IO.ModuleBasedRepositoryActionProvider; using Microsoft.Extensions.Logging.Abstractions; using RepoM.Api.Common; using RepoM.Api.Git; -using RepoM.Api.IO; using RepoM.Api.IO.ExpressionEvaluator; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.ActionMappers; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider.Deserialization; using RepoM.Api.IO.VariableProviders; +using RepoM.Core.Plugin.Common; using VerifyTests; using VerifyXunit; using Xunit; @@ -54,6 +54,7 @@ public RepositorySpecificConfigurationTest() _verifySettings = new VerifySettings(); _verifySettings.UseDirectory("Verified"); + _verifySettings.IgnoreMember(nameof(Repository)); _tempPath = Path.GetTempPath(); _fileSystem = new MockFileSystem(new Dictionary(), "C:\\"); diff --git a/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Verified/RepositorySpecificConfigurationTest.Create_ShouldNotCareAboutMultiSelectRepos_WhenSingleRepo.verified.txt b/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Verified/RepositorySpecificConfigurationTest.Create_ShouldNotCareAboutMultiSelectRepos_WhenSingleRepo.verified.txt index 89737932..fc7fe337 100644 --- a/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Verified/RepositorySpecificConfigurationTest.Create_ShouldNotCareAboutMultiSelectRepos_WhenSingleRepo.verified.txt +++ b/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Verified/RepositorySpecificConfigurationTest.Create_ShouldNotCareAboutMultiSelectRepos_WhenSingleRepo.verified.txt @@ -3,9 +3,12 @@ $type: RepositoryAction, Name: Google 1, Action: { - Type: Action, - Target: ActionBrowserV1Mapper.<>c__DisplayClass6_0, - Method: Void Map(System.Object, System.Object) + $type: DelegateAction, + Action: { + Type: Action, + Target: ActionBrowserV1Mapper.<>c__DisplayClass6_0, + Method: Void Map(System.Object, System.Object) + } }, ExecutionCausesSynchronizing: false, CanExecute: true @@ -14,9 +17,12 @@ $type: RepositoryAction, Name: Google 2, Action: { - Type: Action, - Target: ActionBrowserV1Mapper.<>c__DisplayClass6_0, - Method: Void Map(System.Object, System.Object) + $type: DelegateAction, + Action: { + Type: Action, + Target: ActionBrowserV1Mapper.<>c__DisplayClass6_0, + Method: Void Map(System.Object, System.Object) + } }, ExecutionCausesSynchronizing: false, CanExecute: true @@ -25,9 +31,12 @@ $type: RepositoryAction, Name: Google 4, Action: { - Type: Action, - Target: ActionBrowserV1Mapper.<>c__DisplayClass6_0, - Method: Void Map(System.Object, System.Object) + $type: DelegateAction, + Action: { + Type: Action, + Target: ActionBrowserV1Mapper.<>c__DisplayClass6_0, + Method: Void Map(System.Object, System.Object) + } }, ExecutionCausesSynchronizing: false, CanExecute: true @@ -36,9 +45,12 @@ $type: RepositoryAction, Name: Google 5, Action: { - Type: Action, - Target: ActionBrowserV1Mapper.<>c__DisplayClass6_0, - Method: Void Map(System.Object, System.Object) + $type: DelegateAction, + Action: { + Type: Action, + Target: ActionBrowserV1Mapper.<>c__DisplayClass6_0, + Method: Void Map(System.Object, System.Object) + } }, ExecutionCausesSynchronizing: false, CanExecute: true diff --git a/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Verified/RepositorySpecificConfigurationTest.Create_ShouldProcessSeparator1.verified.txt b/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Verified/RepositorySpecificConfigurationTest.Create_ShouldProcessSeparator1.verified.txt index b87b773a..442ed10f 100644 --- a/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Verified/RepositorySpecificConfigurationTest.Create_ShouldProcessSeparator1.verified.txt +++ b/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Verified/RepositorySpecificConfigurationTest.Create_ShouldProcessSeparator1.verified.txt @@ -3,15 +3,21 @@ $type: RepositoryAction, Name: Google 1, Action: { - Type: Action, - Target: ActionBrowserV1Mapper.<>c__DisplayClass6_0, - Method: Void Map(System.Object, System.Object) + $type: DelegateAction, + Action: { + Type: Action, + Target: ActionBrowserV1Mapper.<>c__DisplayClass6_0, + Method: Void Map(System.Object, System.Object) + } }, ExecutionCausesSynchronizing: false, CanExecute: true }, { $type: RepositorySeparatorAction, + Action: { + $type: NullAction + }, ExecutionCausesSynchronizing: false, CanExecute: true }, @@ -19,9 +25,12 @@ $type: RepositoryAction, Name: Google 2, Action: { - Type: Action, - Target: ActionBrowserV1Mapper.<>c__DisplayClass6_0, - Method: Void Map(System.Object, System.Object) + $type: DelegateAction, + Action: { + Type: Action, + Target: ActionBrowserV1Mapper.<>c__DisplayClass6_0, + Method: Void Map(System.Object, System.Object) + } }, ExecutionCausesSynchronizing: false, CanExecute: true diff --git a/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Verified/RepositorySpecificConfigurationTest.Create_ShouldRespectMultiSelectRepos.verified.txt b/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Verified/RepositorySpecificConfigurationTest.Create_ShouldRespectMultiSelectRepos.verified.txt index 3d23caf4..a1bd0df9 100644 --- a/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Verified/RepositorySpecificConfigurationTest.Create_ShouldRespectMultiSelectRepos.verified.txt +++ b/tests/RepoM.Api.Tests/IO/ModuleBasedRepositoryActionProvider/Verified/RepositorySpecificConfigurationTest.Create_ShouldRespectMultiSelectRepos.verified.txt @@ -3,9 +3,12 @@ $type: RepositoryAction, Name: Google 1, Action: { - Type: Action, - Target: ActionBrowserV1Mapper.<>c__DisplayClass6_0, - Method: Void Map(System.Object, System.Object) + $type: DelegateAction, + Action: { + Type: Action, + Target: ActionBrowserV1Mapper.<>c__DisplayClass6_0, + Method: Void Map(System.Object, System.Object) + } }, ExecutionCausesSynchronizing: false, CanExecute: true @@ -14,9 +17,12 @@ $type: RepositoryAction, Name: Google 5, Action: { - Type: Action, - Target: ActionBrowserV1Mapper.<>c__DisplayClass6_0, - Method: Void Map(System.Object, System.Object) + $type: DelegateAction, + Action: { + Type: Action, + Target: ActionBrowserV1Mapper.<>c__DisplayClass6_0, + Method: Void Map(System.Object, System.Object) + } }, ExecutionCausesSynchronizing: false, CanExecute: true diff --git a/tests/RepoM.Api.Tests/RepoM.Api.Tests.csproj b/tests/RepoM.Api.Tests/RepoM.Api.Tests.csproj index 5a72cb03..81dd4062 100644 --- a/tests/RepoM.Api.Tests/RepoM.Api.Tests.csproj +++ b/tests/RepoM.Api.Tests/RepoM.Api.Tests.csproj @@ -17,9 +17,9 @@ - - - + + + diff --git a/tests/RepoM.Plugin.AzureDevOps.Tests/RepoM.Plugin.AzureDevOps.Tests.csproj b/tests/RepoM.Plugin.AzureDevOps.Tests/RepoM.Plugin.AzureDevOps.Tests.csproj index 43524013..b657a087 100644 --- a/tests/RepoM.Plugin.AzureDevOps.Tests/RepoM.Plugin.AzureDevOps.Tests.csproj +++ b/tests/RepoM.Plugin.AzureDevOps.Tests/RepoM.Plugin.AzureDevOps.Tests.csproj @@ -2,7 +2,6 @@ net6.0 - false @@ -19,9 +18,9 @@ - - - + + + @@ -30,6 +29,7 @@ + @@ -37,8 +37,4 @@ - - - - diff --git a/tests/RepoM.Plugin.LuceneSearch.Tests/RepoM.Plugin.LuceneSearch.Tests.csproj b/tests/RepoM.Plugin.LuceneSearch.Tests/RepoM.Plugin.LuceneSearch.Tests.csproj index 90fef40c..b99d2c58 100644 --- a/tests/RepoM.Plugin.LuceneSearch.Tests/RepoM.Plugin.LuceneSearch.Tests.csproj +++ b/tests/RepoM.Plugin.LuceneSearch.Tests/RepoM.Plugin.LuceneSearch.Tests.csproj @@ -2,7 +2,6 @@ net6.0 - false diff --git a/tests/RepoM.Plugin.SonarCloud.Tests/RepoM.Plugin.SonarCloud.Tests.csproj b/tests/RepoM.Plugin.SonarCloud.Tests/RepoM.Plugin.SonarCloud.Tests.csproj index 99b4343e..2c3ff56c 100644 --- a/tests/RepoM.Plugin.SonarCloud.Tests/RepoM.Plugin.SonarCloud.Tests.csproj +++ b/tests/RepoM.Plugin.SonarCloud.Tests/RepoM.Plugin.SonarCloud.Tests.csproj @@ -2,7 +2,6 @@ net6.0 - false @@ -19,9 +18,9 @@ - - - + + + diff --git a/tests/RepoM.Plugin.Statistics.Tests/DummyEvent.cs b/tests/RepoM.Plugin.Statistics.Tests/DummyEvent.cs new file mode 100644 index 00000000..c3c3afb1 --- /dev/null +++ b/tests/RepoM.Plugin.Statistics.Tests/DummyEvent.cs @@ -0,0 +1,11 @@ +namespace RepoM.Plugin.Statistics.Tests; + +using System; +using RepoM.Plugin.Statistics.Interface; + +internal class DummyEvent : IEvent +{ + public string Repository { get; set; } = null!; + + public DateTime Timestamp { get; set; } +} \ No newline at end of file diff --git a/tests/RepoM.Plugin.Statistics.Tests/Ordering/IntegrationTest.cs b/tests/RepoM.Plugin.Statistics.Tests/Ordering/IntegrationTest.cs new file mode 100644 index 00000000..ea889bb6 --- /dev/null +++ b/tests/RepoM.Plugin.Statistics.Tests/Ordering/IntegrationTest.cs @@ -0,0 +1,82 @@ +namespace RepoM.Plugin.Statistics.Tests.Ordering; + +using System.Collections.Generic; +using FakeItEasy; +using System.IO.Abstractions; +using System.IO.Abstractions.TestingHelpers; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using RepoM.Api.Common; +using RepoM.Core.Plugin.Common; +using SimpleInjector; +using Xunit; +using VerifyXunit; +using EasyTestFileXunit; +using EasyTestFile; +using VerifyTests; +using RepoM.Api.Tests.IO.ModuleBasedRepositoryActionProvider; +using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions.Interfaces; +using RepoM.Core.Plugin.RepositoryOrdering.Configuration; + +[UsesEasyTestFile] +[UsesVerify] +public class IntegrationTest +{ + private readonly IAppDataPathProvider _appDataPathProvider; + private readonly MockFileSystem _fileSystem; + private readonly FilesICompareSettingsService _sut; + private readonly EasyTestFileSettings _testFileSettings; + private readonly VerifySettings _verifySettings; + + public IntegrationTest() + { + var container = new Container(); + new StatisticsPackage().RegisterServices(container); + + _appDataPathProvider = A.Fake(); + _fileSystem = new MockFileSystem(); + container.Register(Lifestyle.Singleton); + container.RegisterSingleton(A.Dummy); + container.RegisterInstance(_appDataPathProvider); + container.RegisterSingleton(A.Dummy); + container.RegisterInstance(_fileSystem); + + A.CallTo(() => _appDataPathProvider.GetAppDataPath()).Returns("C:\\\\dir"); + + container.Verify(); + + _sut = container.GetInstance(); + + _testFileSettings = new EasyTestFileSettings(); + _testFileSettings.UseDirectory("TestFiles"); + _testFileSettings.UseExtension("yml"); + + _verifySettings = new VerifySettings(); + _verifySettings.UseDirectory("Verified"); + } + + [Fact] + public async Task LastOpenedConfiguration() + { + // arrange + await _fileSystem.AddEasyFile("C:\\\\dir\\RepoM.Ordering.yaml", _testFileSettings); + + // act + Dictionary result =_sut.Configuration; + + // assert + await Verifier.Verify(result, _verifySettings); + } + [Fact] + public async Task LastOpenedConfiguration1() + { + // arrange + await _fileSystem.AddEasyFile("C:\\\\dir\\RepoM.Ordering.yaml", _testFileSettings); + + // act + Dictionary result =_sut.Configuration; + + // assert + await Verifier.Verify(result, _verifySettings); + } +} \ No newline at end of file diff --git a/tests/RepoM.Plugin.Statistics.Tests/Ordering/MockFileDataFactory.cs b/tests/RepoM.Plugin.Statistics.Tests/Ordering/MockFileDataFactory.cs new file mode 100644 index 00000000..2832c843 --- /dev/null +++ b/tests/RepoM.Plugin.Statistics.Tests/Ordering/MockFileDataFactory.cs @@ -0,0 +1,34 @@ +namespace RepoM.Plugin.Statistics.Tests.Ordering; + +using System.IO; +using System.IO.Abstractions.TestingHelpers; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using EasyTestFile; +using EasyTestFileXunit; + +internal static class MockFileDataFactory +{ + public static async Task AddEasyFile( + this MockFileSystem fs, + string filename, + EasyTestFileSettings? settings = null, + [CallerFilePath] string sourceFile = "", + [CallerMemberName] string method = "") + { + await using Stream stream = await EasyTestFile.LoadAsStream(settings, sourceFile, method); + fs.AddFile(filename, new MockFileData(StreamToBytes(stream))); + } + + private static byte[] StreamToBytes(Stream input) + { + if (input is MemoryStream ms) + { + return ms.ToArray(); + } + + using var ms1 = new MemoryStream(); + input.CopyTo(ms1); + return ms1.ToArray(); + } +} \ No newline at end of file diff --git a/tests/RepoM.Plugin.Statistics.Tests/Ordering/TestFiles/IntegrationTest.LastOpenedConfiguration.testfile.yml b/tests/RepoM.Plugin.Statistics.Tests/Ordering/TestFiles/IntegrationTest.LastOpenedConfiguration.testfile.yml new file mode 100644 index 00000000..e79e0354 --- /dev/null +++ b/tests/RepoM.Plugin.Statistics.Tests/Ordering/TestFiles/IntegrationTest.LastOpenedConfiguration.testfile.yml @@ -0,0 +1,3 @@ +Name1: + !last-opened-comparer@1 + weight: 1 \ No newline at end of file diff --git a/tests/RepoM.Plugin.Statistics.Tests/Ordering/TestFiles/IntegrationTest.LastOpenedConfiguration1.testfile.yml b/tests/RepoM.Plugin.Statistics.Tests/Ordering/TestFiles/IntegrationTest.LastOpenedConfiguration1.testfile.yml new file mode 100644 index 00000000..f809ce3f --- /dev/null +++ b/tests/RepoM.Plugin.Statistics.Tests/Ordering/TestFiles/IntegrationTest.LastOpenedConfiguration1.testfile.yml @@ -0,0 +1,3 @@ +Name1: + !last-opened-comparer@1 + weight: 123 \ No newline at end of file diff --git a/tests/RepoM.Plugin.Statistics.Tests/Ordering/UsageScoreCalculatorTest.cs b/tests/RepoM.Plugin.Statistics.Tests/Ordering/UsageScoreCalculatorTest.cs new file mode 100644 index 00000000..3a8bb8cb --- /dev/null +++ b/tests/RepoM.Plugin.Statistics.Tests/Ordering/UsageScoreCalculatorTest.cs @@ -0,0 +1,155 @@ +namespace RepoM.Plugin.Statistics.Tests.Ordering; + +using System; +using System.Collections.Generic; +using FakeItEasy; +using FluentAssertions; +using RepoM.Core.Plugin.Repository; +using RepoM.Plugin.Statistics.Ordering; +using Xunit; +using IClock = RepoM.Core.Plugin.Common.IClock; + +public class UsageScoreCalculatorTest +{ + private readonly StatisticsService _service; + private readonly IClock _calculatorClock; + private readonly IRepository _repository; + private readonly ScoreCalculatorConfig _defaultConfig; + private UsageScoreCalculator _sut; + + public UsageScoreCalculatorTest() + { + _repository = A.Fake(); + A.CallTo(() => _repository.SafePath).Returns("DummyValue"); + + _calculatorClock = A.Fake(); + IClock statisticsServiceClock = A.Fake(); + _service = new StatisticsService(statisticsServiceClock); + + var now = new DateTime(2022, 2, 3, 5, 6, 7, 8); + A.CallTo(() => _calculatorClock.Now).Returns(now); + + _defaultConfig = new ScoreCalculatorConfig + { + MaxScore = 100, + Ranges = new List + { + new() + { + Score = 3, + MaxItems = 100, + MaxAge = new TimeSpan(0, 2, 0, 0), + }, + new() + { + Score = 1, + MaxItems = 100, + MaxAge = new TimeSpan(0, 7, 0, 0), + }, + }, + }; + + A.CallTo(() => statisticsServiceClock.Now).ReturnsNextFromSequence( + now.AddHours(-1), + now.AddHours(-2), + now.AddHours(-3)); + + _service.Record(_repository); // now - 1h + _service.Record(_repository); // now - 2h + _service.Record(_repository); // now - 3h + + _sut = new UsageScoreCalculator(_service, _calculatorClock, _defaultConfig); + } + + [Fact] + public void Score_ShouldUseRecordings_WhenCalculating() + { + // arrange + + // act + var result = _sut.Score(_repository); + + // assert + result.Should().Be(7); + } + + [Fact] + public void Score_MaxCount_Scenario1() + { + // arrange + _defaultConfig.MaxScore = 0; + + // act + var result = _sut.Score(_repository); + + // assert + result.Should().Be(0); + } + + [Fact] + public void Score_MaxCount_Scenario2() + { + // arrange + _defaultConfig.MaxScore = 4; + + // act + var result = _sut.Score(_repository); + + // assert + result.Should().Be(4); + } + + [Fact] + public void Score_ShouldReturnZero_WhenNoRangesSpecified() + { + // arrange + _defaultConfig.Ranges.Clear(); + _sut = new UsageScoreCalculator(_service, _calculatorClock, _defaultConfig); + + // act + var result = _sut.Score(_repository); + + // assert + result.Should().Be(0); + } + + [Fact] + public void Score_ShouldReturnZero_RepositoryWasNotFound() + { + // arrange + IRepository r = A.Fake(); + A.CallTo(() => r.SafePath).Returns("OtherDummyValue"); + + // act + var result = _sut.Score(r); + + // assert + result.Should().Be(0); + } + + [Fact] + public void Score_Scenario1() + { + // arrange + _defaultConfig.Ranges[0].MaxItems = 1; + + // act + var result = _sut.Score(_repository); + + // assert + result.Should().Be(5); + } + + [Fact] + public void Score_Scenario2() + { + // arrange + _defaultConfig.Ranges[1].MaxItems = 0; + + // act + var result = _sut.Score(_repository); + + // assert + result.Should().Be(6); + } +} \ No newline at end of file diff --git a/tests/RepoM.Plugin.Statistics.Tests/Ordering/UsageScorerFactoryTest.cs b/tests/RepoM.Plugin.Statistics.Tests/Ordering/UsageScorerFactoryTest.cs new file mode 100644 index 00000000..4017cc01 --- /dev/null +++ b/tests/RepoM.Plugin.Statistics.Tests/Ordering/UsageScorerFactoryTest.cs @@ -0,0 +1,34 @@ +namespace RepoM.Plugin.Statistics.Tests.Ordering; + +using FakeItEasy; +using FluentAssertions; +using RepoM.Core.Plugin.Common; +using RepoM.Core.Plugin.RepositoryOrdering; +using RepoM.Plugin.Statistics.Ordering; +using Xunit; + +public class UsageScorerFactoryTest +{ + private readonly StatisticsService _service; + private readonly IClock _clock; + + public UsageScorerFactoryTest() + { + _clock = A.Fake(); + _service = new StatisticsService(_clock); + } + + [Fact] + public void Create_ShouldReturnInstanceOfUsageScoreCalculator() + { + // arrange + var sut = new UsageScorerFactory(_service, _clock); + var config = new UsageScorerConfigurationV1(); + + // act + IRepositoryScoreCalculator result = sut.Create(config); + + // assert + result.Should().BeOfType(); + } +} \ No newline at end of file diff --git a/tests/RepoM.Plugin.Statistics.Tests/Ordering/Verified/IntegrationTest.LastOpenedConfiguration.verified.txt b/tests/RepoM.Plugin.Statistics.Tests/Ordering/Verified/IntegrationTest.LastOpenedConfiguration.verified.txt new file mode 100644 index 00000000..b33bb04f --- /dev/null +++ b/tests/RepoM.Plugin.Statistics.Tests/Ordering/Verified/IntegrationTest.LastOpenedConfiguration.verified.txt @@ -0,0 +1,6 @@ +{ + Name1: { + $type: LastOpenedConfigurationV1, + Weight: 1 + } +} \ No newline at end of file diff --git a/tests/RepoM.Plugin.Statistics.Tests/Ordering/Verified/IntegrationTest.LastOpenedConfiguration1.verified.txt b/tests/RepoM.Plugin.Statistics.Tests/Ordering/Verified/IntegrationTest.LastOpenedConfiguration1.verified.txt new file mode 100644 index 00000000..187bda01 --- /dev/null +++ b/tests/RepoM.Plugin.Statistics.Tests/Ordering/Verified/IntegrationTest.LastOpenedConfiguration1.verified.txt @@ -0,0 +1,6 @@ +{ + Name1: { + $type: LastOpenedConfigurationV1, + Weight: 123 + } +} \ No newline at end of file diff --git a/tests/RepoM.Plugin.Statistics.Tests/RepoM.Plugin.Statistics.Tests.csproj b/tests/RepoM.Plugin.Statistics.Tests/RepoM.Plugin.Statistics.Tests.csproj new file mode 100644 index 00000000..7fc42ff0 --- /dev/null +++ b/tests/RepoM.Plugin.Statistics.Tests/RepoM.Plugin.Statistics.Tests.csproj @@ -0,0 +1,41 @@ + + + + net6.0 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + diff --git a/tests/RepoM.Plugin.Statistics.Tests/RepositoryActions/RecordStatisticsActionExecutorDecoratorTest.cs b/tests/RepoM.Plugin.Statistics.Tests/RepositoryActions/RecordStatisticsActionExecutorDecoratorTest.cs new file mode 100644 index 00000000..51f2d17c --- /dev/null +++ b/tests/RepoM.Plugin.Statistics.Tests/RepositoryActions/RecordStatisticsActionExecutorDecoratorTest.cs @@ -0,0 +1,78 @@ +namespace RepoM.Plugin.Statistics.Tests.RepositoryActions; + +using System; +using FakeItEasy; +using FluentAssertions; +using RepoM.Core.Plugin.Common; +using RepoM.Core.Plugin.Repository; +using RepoM.Core.Plugin.RepositoryActions; +using RepoM.Core.Plugin.RepositoryActions.Actions; +using RepoM.Plugin.Statistics.RepositoryActions; +using Xunit; + +public class RecordStatisticsActionExecutorDecoratorTest +{ + private readonly IClock _clock; + private readonly IRepository _repository; + private readonly DateTime _now = DateTime.Now; + + public RecordStatisticsActionExecutorDecoratorTest() + { + _clock = A.Dummy(); + A.CallTo(() => _clock.Now).Returns(_now); + _repository = A.Fake(); + A.CallTo(() => _repository.SafePath).Returns("C:/path/repo"); + } + + [Fact] + public void Execute_ShouldCallExecuteOnDecorateeWithSameArguments() + { + // arrange + IActionExecutor decoratee = A.Fake>(); + var service = new StatisticsService(_clock); + var sut = new RecordStatisticsActionExecutorDecorator(decoratee, service); + + // act + var action = new DummyAction(); + sut.Execute(_repository, action); + + // assert + _ = A.CallTo(() => decoratee.Execute(_repository, action)).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void Execute_ShouldRecordWhenTypeIsNotNullType() + { + // arrange + IActionExecutor decoratee = A.Fake>(); + var service = new StatisticsService(_clock); + var sut = new RecordStatisticsActionExecutorDecorator(decoratee, service); + + // act + + var action = new DummyAction(); + sut.Execute(_repository, action); + + // assert + service.GetRecordings(_repository).Should().BeEquivalentTo(new[] { _now, }); + } + + [Fact] + public void Execute_ShouldNotRecordWhenTypeIsNullType() + { + // arrange + IActionExecutor decoratee = A.Fake>(); + var service = new StatisticsService(_clock); + var sut = new RecordStatisticsActionExecutorDecorator(decoratee, service); + + // act + sut.Execute(_repository, NullAction.Instance); + + // assert + service.GetRecordings(_repository).Should().BeEmpty(); + } +} + +public class DummyAction : IAction +{ +} \ No newline at end of file diff --git a/tests/RepoM.Plugin.Statistics.Tests/RepositoryStatisticsTest.Record_ShouldReturnEvent.verified.txt b/tests/RepoM.Plugin.Statistics.Tests/RepositoryStatisticsTest.Record_ShouldReturnEvent.verified.txt new file mode 100644 index 00000000..584ae99d --- /dev/null +++ b/tests/RepoM.Plugin.Statistics.Tests/RepositoryStatisticsTest.Record_ShouldReturnEvent.verified.txt @@ -0,0 +1,4 @@ +{ + Repository: path, + Timestamp: 2022-12-13 15:17:08 Local +} \ No newline at end of file diff --git a/tests/RepoM.Plugin.Statistics.Tests/RepositoryStatisticsTest.cs b/tests/RepoM.Plugin.Statistics.Tests/RepositoryStatisticsTest.cs new file mode 100644 index 00000000..23df53b1 --- /dev/null +++ b/tests/RepoM.Plugin.Statistics.Tests/RepositoryStatisticsTest.cs @@ -0,0 +1,118 @@ +namespace RepoM.Plugin.Statistics.Tests; + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using RepoM.Core.Plugin.Common; +using RepoM.Plugin.Statistics.Interface; +using VerifyXunit; +using Xunit; + +[UsesVerify] +public class RepositoryStatisticsTest +{ + [Fact] + public void Recordings_ShouldBeEmpty_WhenConstructed() + { + // arrange + IClock clock = A.Fake(); + + // act + var sut = new RepositoryStatistics("path", clock); + + // assert + sut.Recordings.Should().BeEmpty(); + } + + [Fact] + public void Apply_ShouldAddTimestampToRecordings_WhenEventIsRepositoryActionRecordedEvent() + { + // arrange + DateTime now = DateTime.Now.AddMinutes(-1000); + IClock clock = A.Fake(); + var sut = new RepositoryStatistics("path", clock); + + // act + sut.Apply(new RepositoryActionRecordedEvent + { + Repository = "path", + Timestamp = now, + }); + + // assert + sut.Recordings.Should().BeEquivalentTo(new[] { now, }); + } + + [Fact] + public void Apply_ShouldAddTimestampToRecordings_WhenEventIsRepositoryActionRecordedEventWithAlreadyOneDateInside() + { + // arrange + DateTime now1 = DateTime.Now.AddMinutes(-1000); + DateTime now2 = DateTime.Now.AddMinutes(-100); + IClock clock = A.Fake(); + var sut = new RepositoryStatistics("path", clock); + sut.Apply(new RepositoryActionRecordedEvent + { + Repository = "path", + Timestamp = now1, + }); + + // act + sut.Apply(new RepositoryActionRecordedEvent + { + Repository = "path", + Timestamp = now2, + }); + + // assert + sut.Recordings.Should().BeEquivalentTo(new[] { now1, now2, }); + } + + [Fact] + public void Apply_ShouldThrow_WhenEventTypeIsWrong() + { + // arrange + IClock clock = A.Fake(); + var sut = new RepositoryStatistics("path", clock); + + // act + Action act = () => sut.Apply(new DummyEvent()); + + // assert + act.Should().Throw(); + } + + [Fact] + public void Record_ShouldAddTimestampToRecording() + { + // arrange + DateTime now = DateTime.Now.AddMinutes(-1000); + IClock clock = A.Fake(); + A.CallTo(() => clock.Now).Returns(now); + var sut = new RepositoryStatistics("path", clock); + + // act + _ = sut.Record(); + + // assert + sut.Recordings.Should().BeEquivalentTo(new[] { now, }); + } + + [Fact] + public async Task Record_ShouldReturnEvent() + { + // arrange + var now = new DateTime(2022, 12, 13, 15, 17, 8, DateTimeKind.Local); + IClock clock = A.Fake(); + A.CallTo(() => clock.Now).Returns(now); + var sut = new RepositoryStatistics("path", clock); + + // act + IEvent evt = sut.Record(); + + // assert + evt.Should().BeOfType(); + await Verifier.Verify(evt).DontScrubDateTimes(); + } +} \ No newline at end of file diff --git a/tests/RepoM.Plugin.Statistics.Tests/StatisticsModuleTest.cs b/tests/RepoM.Plugin.Statistics.Tests/StatisticsModuleTest.cs new file mode 100644 index 00000000..4202e2bd --- /dev/null +++ b/tests/RepoM.Plugin.Statistics.Tests/StatisticsModuleTest.cs @@ -0,0 +1,41 @@ +namespace RepoM.Plugin.Statistics.Tests; + +using System.IO.Abstractions; +using System.IO.Abstractions.TestingHelpers; +using System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using RepoM.Core.Plugin.Common; +using Xunit; +using IClock = RepoM.Core.Plugin.Common.IClock; + +public class StatisticsModuleTest +{ + private readonly IClock _clock; + private readonly IAppDataPathProvider _pathProvider; + private readonly ILogger _logger; + + public StatisticsModuleTest() + { + _clock = A.Fake(); + _pathProvider = A.Fake(); + A.CallTo(() => _pathProvider.GetAppDataPath()).Returns("C:\\data"); + _logger = A.Fake(); + } + + [Fact] + public async Task StartAsync_ShouldInitialize() + { + // arrange + IFileSystem fileSystem = new MockFileSystem(); + var statisticsService = new StatisticsService(_clock); + var sut = new StatisticsModule(statisticsService, _clock, _pathProvider, fileSystem, _logger); + + // act + await sut.StartAsync(); + + // assert + statisticsService.GetRepositories().Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/tests/RepoM.Plugin.Statistics.Tests/StatisticsPackageTest.cs b/tests/RepoM.Plugin.Statistics.Tests/StatisticsPackageTest.cs new file mode 100644 index 00000000..0e762bf5 --- /dev/null +++ b/tests/RepoM.Plugin.Statistics.Tests/StatisticsPackageTest.cs @@ -0,0 +1,50 @@ +namespace RepoM.Plugin.Statistics.Tests; + +using System; +using System.IO.Abstractions; +using FakeItEasy; +using Microsoft.Extensions.Logging; +using RepoM.Core.Plugin.Common; +using SimpleInjector; +using Xunit; + +public class StatisticsPackageTest +{ + [Fact] + public void RegisterServices_ShouldBeSuccessful_WhenExternalDependenciesAreRegistered() + { + // arrange + var container = new Container(); + RegisterExternals(container); + var sut = new StatisticsPackage(); + + // act + sut.RegisterServices(container); + + // assert + // implicit, Verify throws when container is not valid. + container.Verify(VerificationOption.VerifyAndDiagnose); + } + + [Fact] + public void RegisterServices_ShouldFail_WhenExternalDependenciesAreNotRegistered() + { + // arrange + var container = new Container(); + var sut = new StatisticsPackage(); + + // act + sut.RegisterServices(container); + + // assert + Assert.Throws(() => container.Verify(VerificationOption.VerifyAndDiagnose)); + } + + private static void RegisterExternals(Container container) + { + container.RegisterSingleton(A.Dummy); + container.RegisterSingleton(A.Dummy); + container.RegisterSingleton(A.Dummy); + container.RegisterSingleton(A.Dummy); + } +} \ No newline at end of file diff --git a/tests/RepoM.Plugin.Statistics.Tests/TestFramework/VerifierInitializer.cs b/tests/RepoM.Plugin.Statistics.Tests/TestFramework/VerifierInitializer.cs new file mode 100644 index 00000000..6ba48f5e --- /dev/null +++ b/tests/RepoM.Plugin.Statistics.Tests/TestFramework/VerifierInitializer.cs @@ -0,0 +1,15 @@ +namespace RepoM.Plugin.Statistics.Tests.TestFramework; + +using System.Runtime.CompilerServices; +using Argon; +using VerifyTests; + +public static class VerifierInitializer +{ + [ModuleInitializer] + public static void Initialize() + { + VerifierSettings.DisableRequireUniquePrefix(); + VerifierSettings.AddExtraSettings(serializerSettings => serializerSettings.TypeNameHandling = TypeNameHandling.Auto); + } +} \ No newline at end of file diff --git a/tests/Specs/Specs.csproj b/tests/Specs/Specs.csproj index bdced93d..b8e1ebd4 100644 --- a/tests/Specs/Specs.csproj +++ b/tests/Specs/Specs.csproj @@ -17,10 +17,10 @@ - + - + diff --git a/tests/Tests/Git/DefaultRepositoryIgnoreStoreTests.cs b/tests/Tests/Git/DefaultRepositoryIgnoreStoreTests.cs index 43944df0..76a055fc 100644 --- a/tests/Tests/Git/DefaultRepositoryIgnoreStoreTests.cs +++ b/tests/Tests/Git/DefaultRepositoryIgnoreStoreTests.cs @@ -5,7 +5,7 @@ namespace Tests.Git; using Moq; using NUnit.Framework; using RepoM.Api.Git; -using RepoM.Api.IO; +using RepoM.Core.Plugin.Common; public class DefaultRepositoryIgnoreStoreTests { diff --git a/tests/Tests/Helper/FakeClock.cs b/tests/Tests/Helper/FakeClock.cs index cce89439..77e32cad 100644 --- a/tests/Tests/Helper/FakeClock.cs +++ b/tests/Tests/Helper/FakeClock.cs @@ -1,7 +1,7 @@ namespace Tests.Helper; using System; -using RepoM.Api.Common; +using RepoM.Core.Plugin.Common; internal class FakeClock : IClock { diff --git a/tests/Tests/Tests.csproj b/tests/Tests/Tests.csproj index 7ed92447..b4424e79 100644 --- a/tests/Tests/Tests.csproj +++ b/tests/Tests/Tests.csproj @@ -18,9 +18,9 @@ - + - + diff --git a/tests/Tests/UI/RepositoryViewTests.cs b/tests/Tests/UI/RepositoryViewTests.cs index 0fef5b70..f95cde46 100644 --- a/tests/Tests/UI/RepositoryViewTests.cs +++ b/tests/Tests/UI/RepositoryViewTests.cs @@ -10,14 +10,14 @@ namespace Tests.UI; public class RepositoryViewTests { private Repository _repo = null!; - private RepositoryView _view = null!; + private RepositoryViewModel _viewModel = null!; private StatusCharacterMap _statusCharacterMap = null!; [SetUp] public void Setup() { _repo = new RepositoryBuilder().BuildFullFeatured(); - _view = new RepositoryView(_repo, A.Dummy()); + _viewModel = new RepositoryViewModel(_repo, A.Dummy()); _statusCharacterMap = new StatusCharacterMap(); } @@ -26,7 +26,7 @@ public class CtorMethod : RepositoryViewTests [Test] public void Throws_If_Null_Is_Passed_As_Argument() { - Action act = () => _ = new RepositoryView(null!, A.Dummy()); + Action act = () => _ = new RepositoryViewModel(null!, A.Dummy()); act.Should().Throw(); } } @@ -36,14 +36,14 @@ public class AheadByProperty : RepositoryViewTests [Test] public void Returns_The_Repository_Value_As_String() { - _view.AheadBy.Should().Be(_repo.AheadBy.ToString()); + _viewModel.AheadBy.Should().Be(_repo.AheadBy.ToString()); } [Test] public void Returns_An_Empty_String_For_Null() { _repo.AheadBy = null; - _view.AheadBy.Should().BeEmpty(); + _viewModel.AheadBy.Should().BeEmpty(); } } @@ -52,14 +52,14 @@ public class BehindByProperty : RepositoryViewTests [Test] public void Returns_The_Repository_Value_As_String() { - _view.BehindBy.Should().Be(_repo.BehindBy.ToString()); + _viewModel.BehindBy.Should().Be(_repo.BehindBy.ToString()); } [Test] public void Returns_An_Empty_String_For_Null() { _repo.BehindBy = null; - _view.BehindBy.Should().BeEmpty(); + _viewModel.BehindBy.Should().BeEmpty(); } } @@ -68,14 +68,14 @@ public class BranchesProperty : RepositoryViewTests [Test] public void Returns_The_Repository_Value_As_String() { - _view.Branches.Should().ContainInOrder("master", "feature-one", "feature-two"); + _viewModel.Branches.Should().ContainInOrder("master", "feature-one", "feature-two"); } [Test] public void Returns_An_Empty_Array_For_Null() { _repo.Branches = null!; - _view.Branches.Length.Should().Be(0); + _viewModel.Branches.Length.Should().Be(0); } } @@ -84,14 +84,14 @@ public class CurrentBranchProperty : RepositoryViewTests [Test] public void Returns_The_Repository_Value() { - _view.CurrentBranch.Should().Be(_repo.CurrentBranch); + _viewModel.CurrentBranch.Should().Be(_repo.CurrentBranch); } [Test] public void Returns_An_Empty_String_For_Null() { _repo.CurrentBranch = null!; - _view.CurrentBranch.Should().BeEmpty(); + _viewModel.CurrentBranch.Should().BeEmpty(); } } @@ -100,14 +100,14 @@ public class LocalAddedProperty : RepositoryViewTests [Test] public void Returns_The_Repository_Value_As_String() { - _view.LocalAdded.Should().Be(_repo.LocalAdded.ToString()); + _viewModel.LocalAdded.Should().Be(_repo.LocalAdded.ToString()); } [Test] public void Returns_An_Empty_String_For_Null() { _repo.LocalAdded = null; - _view.LocalAdded.Should().BeEmpty(); + _viewModel.LocalAdded.Should().BeEmpty(); } } @@ -116,14 +116,14 @@ public class LocalIgnoredProperty : RepositoryViewTests [Test] public void Returns_The_Repository_Value_As_String() { - _view.LocalIgnored.Should().Be(_repo.LocalIgnored.ToString()); + _viewModel.LocalIgnored.Should().Be(_repo.LocalIgnored.ToString()); } [Test] public void Returns_An_Empty_String_For_Null() { _repo.LocalIgnored = null; - _view.LocalIgnored.Should().BeEmpty(); + _viewModel.LocalIgnored.Should().BeEmpty(); } } @@ -132,14 +132,14 @@ public class StashCountProperty : RepositoryViewTests [Test] public void Returns_The_Repository_Value_As_String() { - _view.StashCount.Should().Be(_repo.StashCount.ToString()); + _viewModel.StashCount.Should().Be(_repo.StashCount.ToString()); } [Test] public void Returns_An_Empty_String_For_Null() { _repo.StashCount = null; - _view.StashCount.Should().BeEmpty(); + _viewModel.StashCount.Should().BeEmpty(); } } @@ -148,14 +148,14 @@ public class LocalMissingProperty : RepositoryViewTests [Test] public void Returns_The_Repository_Value_As_String() { - _view.LocalMissing.Should().Be(_repo.LocalMissing.ToString()); + _viewModel.LocalMissing.Should().Be(_repo.LocalMissing.ToString()); } [Test] public void Returns_An_Empty_String_For_Null() { _repo.LocalMissing = null; - _view.LocalMissing.Should().BeEmpty(); + _viewModel.LocalMissing.Should().BeEmpty(); } } @@ -164,14 +164,14 @@ public class LocalModifiedProperty : RepositoryViewTests [Test] public void Returns_The_Repository_Value_As_String() { - _view.LocalModified.Should().Be(_repo.LocalModified.ToString()); + _viewModel.LocalModified.Should().Be(_repo.LocalModified.ToString()); } [Test] public void Returns_An_Empty_String_For_Null() { _repo.LocalModified = null; - _view.LocalModified.Should().BeEmpty(); + _viewModel.LocalModified.Should().BeEmpty(); } } @@ -180,14 +180,14 @@ public class LocalRemovedProperty : RepositoryViewTests [Test] public void Returns_The_Repository_Value_As_String() { - _view.LocalRemoved.Should().Be(_repo.LocalRemoved.ToString()); + _viewModel.LocalRemoved.Should().Be(_repo.LocalRemoved.ToString()); } [Test] public void Returns_An_Empty_String_For_Null() { _repo.LocalRemoved = null; - _view.LocalRemoved.Should().BeEmpty(); + _viewModel.LocalRemoved.Should().BeEmpty(); } } @@ -196,14 +196,14 @@ public class LocalStagedProperty : RepositoryViewTests [Test] public void Returns_The_Repository_Value_As_String() { - _view.LocalStaged.Should().Be(_repo.LocalStaged.ToString()); + _viewModel.LocalStaged.Should().Be(_repo.LocalStaged.ToString()); } [Test] public void Returns_An_Empty_String_For_Null() { _repo.LocalStaged = null; - _view.LocalStaged.Should().BeEmpty(); + _viewModel.LocalStaged.Should().BeEmpty(); } } @@ -212,14 +212,14 @@ public class LocalUntrackedProperty : RepositoryViewTests [Test] public void Returns_The_Repository_Value_As_String() { - _view.LocalUntracked.Should().Be(_repo.LocalUntracked.ToString()); + _viewModel.LocalUntracked.Should().Be(_repo.LocalUntracked.ToString()); } [Test] public void Returns_An_Empty_String_For_Null() { _repo.LocalUntracked = null; - _view.LocalUntracked.Should().BeEmpty(); + _viewModel.LocalUntracked.Should().BeEmpty(); } } @@ -228,7 +228,7 @@ public class NameProperty : RepositoryViewTests [Test] public void Returns_The_Repository_Value() { - _view.Name.Should().Be(_repo.Name); + _viewModel.Name.Should().Be(_repo.Name); } } @@ -237,7 +237,7 @@ public class PathProperty : RepositoryViewTests [Test] public void Returns_The_Repository_Value() { - _view.Path.Should().Be(_repo.Path); + _viewModel.Path.Should().Be(_repo.Path); } } @@ -247,7 +247,7 @@ public class StatusProperty : RepositoryViewTests public void Returns_The_Compressed_String_From_The_StatusCompressor_Helper_Class() { var expected = new StatusCompressor(_statusCharacterMap).Compress(_repo); - _view.Status.Should().Be(expected); + _viewModel.Status.Should().Be(expected); } } @@ -256,21 +256,21 @@ public class MatchesFilterMethod : RepositoryViewTests [Test] public void Returns_True_If_Filter_Is_Empty() { - _view.MatchesFilter("").Should().Be(true); + _viewModel.MatchesFilter("").Should().Be(true); } [Test] public void Can_Filter_By_Name_Implicit() { _repo.Name = "Hello World"; - _view.MatchesFilter("lo wo").Should().Be(true); + _viewModel.MatchesFilter("lo wo").Should().Be(true); } [Test] public void Can_Filter_By_Name_Explicit() { _repo.Name = "Hello World"; - _view.MatchesFilter("n lo wo").Should().Be(true); + _viewModel.MatchesFilter("n lo wo").Should().Be(true); } [Test] @@ -278,7 +278,7 @@ public void Can_Filter_By_Branch() { _repo.Name = "No Match Here"; _repo.CurrentBranch = "feature/Test"; - _view.MatchesFilter("b feat").Should().Be(true); + _viewModel.MatchesFilter("b feat").Should().Be(true); } [Test] @@ -286,14 +286,14 @@ public void Can_Filter_By_Path() { _repo.Name = "No Match Here"; _repo.Path = @"C:\Test\Path"; - _view.MatchesFilter(@"p C:\").Should().Be(true); + _viewModel.MatchesFilter(@"p C:\").Should().Be(true); } [Test] public void Returns_True_If_Filter_Is_Empty_Except_Prefix() { // "n ", "b ", "p " can be used to filter for name, branch and path - _view.MatchesFilter("b ").Should().Be(true); + _viewModel.MatchesFilter("b ").Should().Be(true); } [Test] @@ -301,7 +301,7 @@ public void Returns_False_If_Prefix_Misses_Space() { // should be interpreted as "b" search term without prefix _repo.Name = "xyz"; - _view.MatchesFilter("b").Should().Be(false); + _viewModel.MatchesFilter("b").Should().Be(false); } [Test] @@ -309,7 +309,7 @@ public void Returns_False_If_Prefix_Comes_With_Two_Spaces() { // trimming "b " leads to " master" which is not trimmed by design _repo.CurrentBranch = "master"; - _view.MatchesFilter("b master").Should().Be(false); + _viewModel.MatchesFilter("b master").Should().Be(false); } [Test] @@ -317,7 +317,7 @@ public void Returns_True_For_ToDo_Filter_With_UnpushedChanges() { _repo.StashCount = 1; _repo.HasUnpushedChanges.Should().Be(true); - _view.MatchesFilter("todo").Should().Be(true); + _viewModel.MatchesFilter("todo").Should().Be(true); } [Test] @@ -325,7 +325,7 @@ public void Returns_False_For_ToDo_Filter_Without_UnpushedChanges() { _repo = new Repository(); _repo.HasUnpushedChanges.Should().Be(false); - new RepositoryView(_repo, A.Dummy()).MatchesFilter("todo").Should().Be(false); + new RepositoryViewModel(_repo, A.Dummy()).MatchesFilter("todo").Should().Be(false); } } @@ -334,14 +334,14 @@ public class WasFoundProperty : RepositoryViewTests [Test] public void Returns_The_Repository_Value() { - _view.WasFound.Should().Be(_repo.WasFound); + _viewModel.WasFound.Should().Be(_repo.WasFound); } [Test] public void Returns_False_If_Path_Is_Empty() { _repo.Path = ""; - _view.WasFound.Should().BeFalse(); + _viewModel.WasFound.Should().BeFalse(); } } @@ -350,7 +350,7 @@ public class GetHashCodeMethod : RepositoryViewTests [Test] public void Returns_The_Repository_Value() { - _view.GetHashCode().Should().Be(_repo.GetHashCode()); + _viewModel.GetHashCode().Should().Be(_repo.GetHashCode()); } } } \ No newline at end of file