diff --git a/Directory.Packages.props b/Directory.Packages.props index baecd84..4861562 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,14 +5,10 @@ - - - - - + \ No newline at end of file diff --git a/README.md b/README.md index 1375141..0f04862 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This is designed to helps to keep track of git branches and their azure devops w - Extract the release zip file to any folder - Add the "install" folder to your path environment variables - Edit `appSettings.json` in the "install folder" and enter your `AzureDevopsOrgUri` (where your work items are stored) -- Start `RepoCleaner.exe --config` to enter _config mode_ and enter an azure devops personal access token that has at least _Work Items_ and _Code_ read access +- Start `RepoCleaner.exe config` to enter _config mode_ and enter an azure devops personal access token that has at least _Work Items_ and _Code_ read access - [Configure you powershell to use UTF8 encoding](docs/doc.md#powershell), otherwise some icons will not be able to be displayed ## Quick Usage diff --git a/docs/doc.md b/docs/doc.md index c1f121f..7959cf6 100644 --- a/docs/doc.md +++ b/docs/doc.md @@ -53,7 +53,7 @@ To access work items and code on a azure devops server add an azure devops perso The token will be stored locally on your computer as a generic Windows credential with the name _Develix:RepoCleanerAzureDevopsToken_. ```ps -RepoCleaner.exe --config +RepoCleaner.exe config ``` ![Enter Azure Devops Token](docs-enter_token.png) @@ -108,7 +108,7 @@ The default colors are approximately | Color | Status | | ----- | -------------------------------------- | | ⚪ | Work item is in a _not started_ status | -| 🔵 | Work item is in a _precessing_ status | +| 🔵 | Work item is in a _processing_ status | | 🟢 | Work item is in a _done_ status | | ⚫ | No work item found | diff --git a/src/RepoCleaner/App.cs b/src/RepoCleaner/App.cs deleted file mode 100644 index f5b3c8f..0000000 --- a/src/RepoCleaner/App.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Develix.RepoCleaner.ConsoleComponents; -using Develix.RepoCleaner.Model; -using Develix.RepoCleaner.Store.ConsoleSettingsUseCase; -using Develix.RepoCleaner.Store.RepositoryInfoUseCase; -using Fluxor; -using Spectre.Console; - -namespace Develix.RepoCleaner; - -public class App -{ - private const string CredentialName = "Develix:RepoCleanerAzureDevopsToken"; - - private readonly IStore store; - private readonly IDispatcher dispatcher; - private readonly IState repositoryInfoState; - private readonly IState consoleSettingsState; - - public App( - IStore store, - IDispatcher dispatcher, - IState repositoryInfoState, - IState consoleSettingsState) - { - this.store = store ?? throw new ArgumentNullException(nameof(store)); - this.dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); - this.repositoryInfoState = repositoryInfoState ?? throw new ArgumentNullException(nameof(repositoryInfoState)); - this.consoleSettingsState = consoleSettingsState ?? throw new ArgumentNullException(nameof(consoleSettingsState)); - } - - public async Task Run(ConsoleArguments consoleArguments, AppSettings appSettings) - { - try - { - await store.InitializeAsync(); - - if (consoleArguments.Config) - { - Config.Show(CredentialName, consoleSettingsState, dispatcher); - return; - } - - await InitConsole(consoleArguments, appSettings); - LogErrors(repositoryInfoState); - ShowOverviewTable(); - if (consoleSettingsState.Value.ShowDeletePrompt) - ShowDeletePrompt(); - - } - catch (Exception ex) - { - AnsiConsole.WriteException(ex); - } - } - - private async Task InitConsole(ConsoleArguments consoleArguments, AppSettings appSettings) - { - var initConsole = new Init(CredentialName, dispatcher, repositoryInfoState, store); - await initConsole.Execute(consoleArguments, appSettings); - } - - private static void LogErrors(IState repositoryInfoState) - { - foreach (var error in repositoryInfoState.Value.ErrorMessages) - AnsiConsole.MarkupLine(error); - } - - private void ShowOverviewTable() - { - var overviewTable = new OverviewTable(repositoryInfoState.Value, consoleSettingsState.Value); - AnsiConsole.Write(overviewTable.GetOverviewTable()); - } - - private void ShowDeletePrompt() - { - var branchesToDelete = Delete.Prompt(repositoryInfoState); - Delete.Execute(branchesToDelete, repositoryInfoState); - } -} diff --git a/src/RepoCleaner/ConsoleComponents/Cli/AzdoClient.cs b/src/RepoCleaner/ConsoleComponents/Cli/AzdoClient.cs new file mode 100644 index 0000000..ccba182 --- /dev/null +++ b/src/RepoCleaner/ConsoleComponents/Cli/AzdoClient.cs @@ -0,0 +1,43 @@ +using Develix.AzureDevOps.Connector.Service; +using Develix.CredentialStore.Win32; +using Develix.Essentials.Core; +using Develix.RepoCleaner.Model; + +namespace Develix.RepoCleaner.ConsoleComponents.Cli; + +internal static class AzdoClient +{ + public const string CredentialName = "Develix:RepoCleanerAzureDevopsToken"; + + public static async Task Login( + IReposService reposService, + IWorkItemService workItemService, + RepoCleanerSettings settings, + AppSettings appSettings) + { + IEnumerable> loginActions = settings.IncludePullRequests + ? [LoginWorkItemService(workItemService, appSettings.AzureDevopsOrgUri), LoginReposService(reposService, appSettings.AzureDevopsOrgUri)] + : [LoginWorkItemService(workItemService, appSettings.AzureDevopsOrgUri)]; + + var loginResults = await Task.WhenAll(loginActions); + if (loginResults.Where(r => !r.Valid).Select(r => r.Message).ToList() is { Count: >= 1 } errorMessages) + return Result.Fail($"Login failed. {Environment.NewLine}{string.Join(Environment.NewLine, errorMessages)}"); + return Result.Ok(); + } + + private static async Task LoginWorkItemService(IWorkItemService workItemService, Uri azureDevopsUri) + { + var credential = CredentialManager.Get(CredentialName); + return credential.Valid + ? await workItemService.Initialize(azureDevopsUri, credential.Value.Password!) + : Result.Fail(credential.Message); + } + + private static async Task LoginReposService(IReposService reposService, Uri azureDevopsUri) + { + var credential = CredentialManager.Get(CredentialName); + return credential.Valid + ? await reposService.Initialize(azureDevopsUri, credential.Value.Password!) + : Result.Fail(credential.Message); + } +} diff --git a/src/RepoCleaner/ConsoleComponents/Cli/ConfigCommand.cs b/src/RepoCleaner/ConsoleComponents/Cli/ConfigCommand.cs new file mode 100644 index 0000000..64a4d8f --- /dev/null +++ b/src/RepoCleaner/ConsoleComponents/Cli/ConfigCommand.cs @@ -0,0 +1,31 @@ +using Develix.CredentialStore.Win32; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace Develix.RepoCleaner.ConsoleComponents.Cli; + +internal class ConfigCommand : Command +{ + public override int Execute(CommandContext context) + { + var prompt = new TextPrompt("Enter [green]azure devops token[/]") + .PromptStyle("red") + .Secret(); + + var token = AnsiConsole.Prompt(prompt); + + var credential = new Credential("token", token, AzdoClient.CredentialName); + var crudResult = CredentialManager.CreateOrUpdate(credential); + + if (!crudResult.Valid) + { + var message = $""" + Storing credentials failed. + {crudResult.Message}"; + """; + AnsiConsole.WriteLine(message); + return 1; + } + return 0; + } +} diff --git a/src/RepoCleaner/ConsoleComponents/Cli/Infrastructure/TypeRegistrar.cs b/src/RepoCleaner/ConsoleComponents/Cli/Infrastructure/TypeRegistrar.cs new file mode 100644 index 0000000..f872b6d --- /dev/null +++ b/src/RepoCleaner/ConsoleComponents/Cli/Infrastructure/TypeRegistrar.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.DependencyInjection; +using Spectre.Console.Cli; + +namespace Develix.RepoCleaner.ConsoleComponents.Cli.Infrastructure; + +public sealed class TypeRegistrar(IServiceCollection builder) : ITypeRegistrar +{ + private readonly IServiceCollection builder = builder; + + public ITypeResolver Build() => new TypeResolver(builder.BuildServiceProvider()); + + public void Register(Type service, Type implementation) + { + _ = builder.AddSingleton(service, implementation); + } + + public void RegisterInstance(Type service, object implementation) + { + _ = builder.AddSingleton(service, implementation); + } + + public void RegisterLazy(Type service, Func func) + { + ArgumentNullException.ThrowIfNull(func); + builder.AddSingleton(service, (provider) => func()); + } +} diff --git a/src/RepoCleaner/ConsoleComponents/Cli/Infrastructure/TypeResolver.cs b/src/RepoCleaner/ConsoleComponents/Cli/Infrastructure/TypeResolver.cs new file mode 100644 index 0000000..4b80622 --- /dev/null +++ b/src/RepoCleaner/ConsoleComponents/Cli/Infrastructure/TypeResolver.cs @@ -0,0 +1,21 @@ +using Spectre.Console.Cli; + +namespace Develix.RepoCleaner.ConsoleComponents.Cli.Infrastructure; + +public sealed class TypeResolver(IServiceProvider provider) : ITypeResolver, IDisposable +{ + private readonly IServiceProvider provider = provider ?? throw new ArgumentNullException(nameof(provider)); + + public object? Resolve(Type? type) + { + return type is null + ? null + : provider.GetService(type); + } + + public void Dispose() + { + if (provider is IDisposable disposable) + disposable.Dispose(); + } +} diff --git a/src/RepoCleaner/ConsoleComponents/Cli/RepoCleanerCommand.cs b/src/RepoCleaner/ConsoleComponents/Cli/RepoCleanerCommand.cs new file mode 100644 index 0000000..c62a5ba --- /dev/null +++ b/src/RepoCleaner/ConsoleComponents/Cli/RepoCleanerCommand.cs @@ -0,0 +1,74 @@ +using Develix.AzureDevOps.Connector.Model; +using Develix.AzureDevOps.Connector.Service; +using Develix.Essentials.Core; +using Develix.RepoCleaner.Git.Model; +using Develix.RepoCleaner.Model; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace Develix.RepoCleaner.ConsoleComponents.Cli; + +internal class RepoCleanerCommand(AppSettings appSettings, IReposService reposService, IWorkItemService workItemService) + : AsyncCommand +{ + private readonly AppSettings appSettings = appSettings; + private readonly IReposService reposService = reposService; + private readonly IWorkItemService workItemService = workItemService; + + public override async Task ExecuteAsync(CommandContext context, RepoCleanerSettings settings) + { + var status = await AnsiConsole + .Status() + .Spinner(Spinner.Known.Dots) + .StartAsync("Starting RepoCleaner", async ctx => + { + ctx.Status("Logging in"); + var loginResult = await AzdoClient.Login(reposService, workItemService, settings, appSettings); + if (!Validate(loginResult)) + return 1; + + ctx.Status("Getting repository"); + var repositoryResult = RepositoryInfo.Get(settings, appSettings); + if (!Validate(repositoryResult)) + return 2; + + ctx.Status("Getting work items"); + var workItemsIds = RepositoryInfo.GetRelatedWorkItemIds(repositoryResult.Value); + var workItemsResult = await workItemService.GetWorkItems(workItemsIds, settings.IncludePullRequests); + if (!Validate(workItemsResult)) + return 3; + + ctx.Status("Rendering output"); + var table = new OverviewTable(appSettings, settings); + var renderedTable = table.GetOverviewTable(workItemsResult.Value, repositoryResult.Value); + AnsiConsole.Write(renderedTable); + + ctx.Status("Waiting for branches to delete"); + if (settings.Delete) + ExecuteDelete(repositoryResult.Value, workItemsResult.Value); + + return 0; + }); + return status; + } + + private static void ExecuteDelete(Repository repository, IEnumerable workItems) + { + var branchesToDelete = Delete.Prompt(repository, workItems); + Delete.Execute(repository, branchesToDelete); + } + + private static bool Validate(IResult result) + { + if (!result.Valid) + { + var message = $""" + An error occurred. + {result.Message} + """; + AnsiConsole.MarkupLine(message); + return false; + } + return true; + } +} diff --git a/src/RepoCleaner/ConsoleComponents/Cli/RepoCleanerSettings.cs b/src/RepoCleaner/ConsoleComponents/Cli/RepoCleanerSettings.cs new file mode 100644 index 0000000..1dba57e --- /dev/null +++ b/src/RepoCleaner/ConsoleComponents/Cli/RepoCleanerSettings.cs @@ -0,0 +1,22 @@ +using Develix.RepoCleaner.Model; +using Spectre.Console.Cli; + +namespace Develix.RepoCleaner.ConsoleComponents.Cli; + +internal class RepoCleanerSettings : CommandSettings +{ + [CommandOption("-d|--delete")] + public bool Delete { get; set; } + + [CommandOption("-p|--path ")] + public string? Path { get; set; } + + [CommandOption("-b|--branch ")] + public BranchSourceKind BranchSource { get; set; } = BranchSourceKind.Local; + + [CommandOption("--pr|--pull-requests")] + public bool IncludePullRequests { get; set; } + + [CommandOption("--author")] + public bool IncludeAuthor { get; set; } +} diff --git a/src/RepoCleaner/ConsoleComponents/Cli/RepositoryInfo.cs b/src/RepoCleaner/ConsoleComponents/Cli/RepositoryInfo.cs new file mode 100644 index 0000000..ab0659a --- /dev/null +++ b/src/RepoCleaner/ConsoleComponents/Cli/RepositoryInfo.cs @@ -0,0 +1,26 @@ +using Develix.Essentials.Core; +using Develix.RepoCleaner.Git; +using Develix.RepoCleaner.Git.Model; +using Develix.RepoCleaner.Model; + +namespace Develix.RepoCleaner.ConsoleComponents.Cli; + +internal static class RepositoryInfo +{ + public static Result Get(RepoCleanerSettings settings, AppSettings appSettings) + { + var path = settings.Path ?? Directory.GetCurrentDirectory(); + return settings.BranchSource switch + { + BranchSourceKind.Local => Reader.GetLocalRepo(path, appSettings.ExcludedBranches), + BranchSourceKind.Remote => Reader.GetRemoteRepo(path, appSettings.ExcludedBranches), + BranchSourceKind.All => Reader.GetRepo(path, appSettings.ExcludedBranches), + _ => throw new NotSupportedException($"The {nameof(BranchSourceKind)} '{settings.BranchSource}' is not supported!"), + }; + } + + public static IReadOnlyCollection GetRelatedWorkItemIds(Repository repository) + { + return repository.Branches.Select(b => b.RelatedWorkItemId).OfType().ToList(); + } +} diff --git a/src/RepoCleaner/ConsoleComponents/Config.cs b/src/RepoCleaner/ConsoleComponents/Config.cs deleted file mode 100644 index e8799ba..0000000 --- a/src/RepoCleaner/ConsoleComponents/Config.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Develix.RepoCleaner.Store; -using Develix.RepoCleaner.Store.ConsoleSettingsUseCase; -using Develix.RepoCleaner.Utils; -using Fluxor; -using Spectre.Console; - -namespace Develix.RepoCleaner.ConsoleComponents; -public static class Config -{ - public static void Show(string credentialName, IState consoleSettingsState, IDispatcher dispatcher) - { - var token = AnsiConsole.Prompt(new TextPrompt("Enter [green]azure devops token[/]") - .PromptStyle("red") - .Secret()); - AnsiConsole - .Status() - .Start("Storing credentials", - async (ctx) => - { - var action = new ConfigureCredentialsAction(credentialName, token); - dispatcher.Dispatch(action); - await AsyncHelper.WaitUntilAsync(() => !consoleSettingsState.Value.Configuring, 100, 2000, default); - ctx.Status("Credentials initialized"); - }); - } -} diff --git a/src/RepoCleaner/ConsoleComponents/Delete.cs b/src/RepoCleaner/ConsoleComponents/Delete.cs index d23e92a..6b8fe3e 100644 --- a/src/RepoCleaner/ConsoleComponents/Delete.cs +++ b/src/RepoCleaner/ConsoleComponents/Delete.cs @@ -1,30 +1,29 @@ -using Develix.RepoCleaner.Git; +using Develix.AzureDevOps.Connector.Model; +using Develix.RepoCleaner.Git; using Develix.RepoCleaner.Git.Model; -using Develix.RepoCleaner.Store.RepositoryInfoUseCase; -using Fluxor; using Spectre.Console; namespace Develix.RepoCleaner.ConsoleComponents; public static class Delete { - public static IReadOnlyList Prompt(IState repositoryInfoState) + public static IReadOnlyList Prompt(Repository repository, IEnumerable workItems) { - var deletableBranches = repositoryInfoState.Value.Repository.Branches.Where(b => IsDeletable(b)).ToList(); + var deletableBranches = repository.Branches.Where(b => IsDeletable(b)).ToList(); if (deletableBranches.Count == 0) { AnsiConsole.MarkupLine("[grey30]No branches can be deleted.[/]"); - return Array.Empty(); + return []; } - return DeleteBranchSelectionPrompt.Show(deletableBranches, repositoryInfoState); + return Show(repository, workItems, deletableBranches); static bool IsDeletable(Branch b) => !b.IsRemote && !b.IsCurrent; } - public static void Execute(IReadOnlyCollection branchesToDelete, IState repositoryInfoState) + public static void Execute(Repository repository, IReadOnlyCollection branchesToDelete) { - using var handler = new Handler(repositoryInfoState.Value.Repository); + using var handler = new Handler(repository); foreach (var branch in branchesToDelete) { var result = handler.TryDeleteBranch(branch); @@ -34,4 +33,35 @@ public static void Execute(IReadOnlyCollection branchesToDelete, IState< AnsiConsole.MarkupLine(message); } } + + private static List Show( + Repository repository, + IEnumerable workItems, + IReadOnlyCollection deletableBranches) + { + var instructionText = + "[grey30](Press [blue][/] to toggle deletion of a branch, " + + "[green][/] to delete selected branches.)[/]"; + var nonDeletableCount = repository.Branches.Count - deletableBranches.Count; + if (nonDeletableCount > 1) // current branch is never deletable + instructionText += $"{Environment.NewLine}[grey30]Remote branches cannot be deleted and are not shown here[/]"; + return AnsiConsole.Prompt( + new MultiSelectionPrompt() + .UseConverter((b) => GetDisplayText(b, workItems)) + .Title("Branches to delete?") + .NotRequired() + .PageSize(6) + .MoreChoicesText("[grey30](Move up and down to reveal more branches)[/]") + .InstructionsText(instructionText) + .AddChoices(deletableBranches)); + } + + private static string GetDisplayText(Branch branch, IEnumerable workItems) + { + var workItem = workItems.FirstOrDefault(wi => wi.Id == branch.RelatedWorkItemId); + var displayText = workItem is null + ? branch.FriendlyName + : $"{branch.FriendlyName} [{workItem.Title}]"; + return displayText.EscapeMarkup(); + } } diff --git a/src/RepoCleaner/ConsoleComponents/DeleteBranchSelectionPrompt.cs b/src/RepoCleaner/ConsoleComponents/DeleteBranchSelectionPrompt.cs deleted file mode 100644 index 07e4c49..0000000 --- a/src/RepoCleaner/ConsoleComponents/DeleteBranchSelectionPrompt.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Develix.RepoCleaner.Git.Model; -using Develix.RepoCleaner.Store.RepositoryInfoUseCase; -using Fluxor; -using Spectre.Console; - -namespace Develix.RepoCleaner.ConsoleComponents; - -public static class DeleteBranchSelectionPrompt -{ - public static IReadOnlyList Show(IReadOnlyList deletableBranches, IState repositoryInfoState) - { - var instructionText = - "[grey30](Press [blue][/] to toggle deletion of a branch, " + - "[green][/] to delete selected branches.)[/]"; - var nonDeletableCount = repositoryInfoState.Value.Repository.Branches.Count - deletableBranches.Count; - if (nonDeletableCount > 1) // current branch is never deletable - instructionText += $"{Environment.NewLine}[grey30]Remote braches cannot be deleted and are not shown here[/]"; - return AnsiConsole.Prompt( - new MultiSelectionPrompt() - .UseConverter((b) => GetDisplayText(b, repositoryInfoState)) - .Title("Branches to delete?") - .NotRequired() - .PageSize(6) - .MoreChoicesText("[grey30](Move up and down to reveal more branches)[/]") - .InstructionsText(instructionText) - .AddChoices(deletableBranches)); - } - - private static string GetDisplayText(Branch branch, IState repositoryInfoState) - { - var workItem = repositoryInfoState.Value.WorkItems.FirstOrDefault(wi => wi.Id == branch.RelatedWorkItemId); - var displayText = workItem is null - ? branch.FriendlyName - : $"{branch.FriendlyName} [{workItem.Title}]"; - return displayText.EscapeMarkup(); - } -} diff --git a/src/RepoCleaner/ConsoleComponents/Init.cs b/src/RepoCleaner/ConsoleComponents/Init.cs deleted file mode 100644 index 106244e..0000000 --- a/src/RepoCleaner/ConsoleComponents/Init.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Develix.RepoCleaner.Model; -using Develix.RepoCleaner.Store; -using Develix.RepoCleaner.Store.RepositoryInfoUseCase; -using Develix.RepoCleaner.Utils; -using Fluxor; -using Spectre.Console; - -namespace Develix.RepoCleaner.ConsoleComponents; - -public class Init -{ - private readonly string credentialName; - private readonly IDispatcher dispatcher; - private readonly IState repositoryInfoState; - - public Init(string credentialName, IDispatcher dispatcher, IState repositoryInfoState, IStore store) - { - this.dispatcher = dispatcher; - this.repositoryInfoState = repositoryInfoState; - this.credentialName = credentialName; - } - - public async Task Execute(ConsoleArguments consoleArguments, AppSettings appSettings) - { - await AnsiConsole.Status() - .StartAsync("Loading", async ctx => - { - ctx.Spinner(Spinner.Known.Dots); - InitConsoleSettings(consoleArguments, appSettings, ctx); - await LoginServices(ctx); - await InitRepository(consoleArguments, ctx); - ctx.Status = "Done"; - }); - } - - private void InitConsoleSettings(ConsoleArguments consoleArguments, AppSettings appSettings, StatusContext statusContext) - { - statusContext.Status = "Initializing Console Settings"; - dispatcher.Dispatch(new SetConsoleSettingsAction(consoleArguments, appSettings)); - } - - private async Task LoginServices(StatusContext statusContext) - { - statusContext.Status = "Login Services"; - dispatcher.Dispatch(new LoginServicesAction(credentialName)); - await AsyncHelper.WaitUntilAsync( - () => IsFinalState(repositoryInfoState.Value.WorkItemServiceState), - 100, - 30000, - default); - - static bool IsFinalState(ServiceConnectionState state) => state == ServiceConnectionState.Connected || state == ServiceConnectionState.FailedToConnect; - } - - private async Task InitRepository(ConsoleArguments consoleArguments, StatusContext statusContext) - { - statusContext.Status = "Initializing repository and work items"; - dispatcher.Dispatch(new InitRepositoryAction(consoleArguments.Branches)); - var waitRepositoryLoaded = AsyncHelper.WaitUntilAsync(() => repositoryInfoState.Value.RepositoryLoaded, 100, 30000, default) - .ContinueWith(t => statusContext.Status = "Initializing work items"); - var waitWorkItemsLoaded = AsyncHelper.WaitUntilAsync(() => repositoryInfoState.Value.WorkItemsLoaded, 100, 30000, default) - .ContinueWith(t => statusContext.Status = "Initializing repository"); - await Task.WhenAll(waitRepositoryLoaded, waitWorkItemsLoaded); - } -} diff --git a/src/RepoCleaner/ConsoleComponents/OverviewTable.cs b/src/RepoCleaner/ConsoleComponents/OverviewTable.cs index 7e0ba2a..8a5dc42 100644 --- a/src/RepoCleaner/ConsoleComponents/OverviewTable.cs +++ b/src/RepoCleaner/ConsoleComponents/OverviewTable.cs @@ -1,31 +1,25 @@ using Develix.AzureDevOps.Connector.Model; +using Develix.RepoCleaner.ConsoleComponents.Cli; using Develix.RepoCleaner.Git.Model; -using Develix.RepoCleaner.Store.ConsoleSettingsUseCase; -using Develix.RepoCleaner.Store.RepositoryInfoUseCase; +using Develix.RepoCleaner.Model; using Spectre.Console; using Spectre.Console.Rendering; namespace Develix.RepoCleaner.ConsoleComponents; -internal class OverviewTable +internal class OverviewTable(AppSettings appSettings, RepoCleanerSettings settings) { - private readonly RepositoryInfoState repositoryInfoState; - private readonly ConsoleSettingsState consoleSettingsState; + private readonly AppSettings appSettings = appSettings; + private readonly RepoCleanerSettings settings = settings; - public OverviewTable(RepositoryInfoState repositoryInfoState, ConsoleSettingsState consoleSettingsState) + public IRenderable GetOverviewTable(IEnumerable workItems, Repository repository) { - this.repositoryInfoState = repositoryInfoState ?? throw new ArgumentNullException(nameof(repositoryInfoState)); - this.consoleSettingsState = consoleSettingsState ?? throw new ArgumentNullException(nameof(consoleSettingsState)); - } - - public IRenderable GetOverviewTable() - { - var teamProjects = repositoryInfoState.WorkItems + var teamProjects = workItems .Select(wi => wi.TeamProject) .Where(tp => !string.IsNullOrWhiteSpace(tp)) .Distinct() .ToList(); - var tableRows = GetTableRows(teamProjects.Count); + var tableRows = GetTableRows(repository, workItems, teamProjects.Count); var table = CreateTable(tableRows.FirstOrDefault()); foreach (var row in tableRows.Select(tr => tr.GetRowData())) table.AddRow(row); @@ -33,39 +27,44 @@ public IRenderable GetOverviewTable() return GetDisplay(teamProjects, table); } - private IReadOnlyList GetTableRows(int numberOfTeamProject) + private List GetTableRows( + Repository repository, + IEnumerable workItems, + int numberOfTeamProject) { - return repositoryInfoState.Repository + return repository .Branches - .Select(b => GetTableRow(b, numberOfTeamProject)) - .SelectMany(x => x) + .Select(b => GetTableRow(b, workItems, numberOfTeamProject)) + .SelectMany(tr => tr) .ToList(); } - private IEnumerable GetTableRow(Branch branch, int numberOfTeamProjects) + private IEnumerable GetTableRow( + Branch branch, + IEnumerable workItems, + int numberOfTeamProjects) { - var workItem = repositoryInfoState.WorkItems.FirstOrDefault(wi => wi.Id == branch.RelatedWorkItemId); + var workItem = workItems.FirstOrDefault(wi => wi.Id == branch.RelatedWorkItemId); var dataRow = GetDataRow(branch, numberOfTeamProjects, workItem); yield return dataRow; - foreach (var pullRequest in workItem?.PullRequests.OrderBy(pr => pr.Id).AsEnumerable() ?? Array.Empty()) - { + + foreach (var pullRequest in workItem?.PullRequests.OrderBy(pr => pr.Id).AsEnumerable() ?? []) yield return new OverviewTablePullRequest(dataRow, pullRequest); - } - if (consoleSettingsState.ShowLastCommitAuthor) + if (settings.IncludeAuthor) yield return new OverviewTableRowAuthor(dataRow, branch.HeadCommitAuthor); } - private OverviewTableRowBase GetDataRow(Branch branch, int numberOfTeamProjects, WorkItem? workItem) + private OverviewTableRow GetDataRow(Branch branch, int numberOfTeamProjects, WorkItem? workItem) { return numberOfTeamProjects switch { - <= 1 => new OverviewTableRow(branch, workItem, consoleSettingsState.WorkItemTypeIcons), + <= 1 => new OverviewTableRow(branch, workItem, appSettings.WorkItemTypeIcons), >= 2 => new OverviewTableRowWithProject( branch, workItem, - consoleSettingsState.WorkItemTypeIcons, - consoleSettingsState.ShortProjectNames), + appSettings.WorkItemTypeIcons, + appSettings.ShortProjectNames), }; } @@ -73,13 +72,13 @@ private static Table CreateTable(OverviewTableRowBase? rowTemplate) { var table = new Table(); table.Border(TableBorder.None); - foreach (var columnTitle in rowTemplate?.GetColumns() ?? Array.Empty()) + foreach (var columnTitle in rowTemplate?.GetColumns() ?? []) table.AddColumn($"[bold]{columnTitle}[/]"); return table; } - private static IRenderable GetDisplay(List teamProjects, Table table) + private static Panel GetDisplay(List teamProjects, Table table) { IRenderable outputDisplay = table.Columns.Count > 0 ? table diff --git a/src/RepoCleaner/ConsoleComponents/OverviewTableColumnAttribute.cs b/src/RepoCleaner/ConsoleComponents/OverviewTableColumnAttribute.cs index 53915f6..c2078ef 100644 --- a/src/RepoCleaner/ConsoleComponents/OverviewTableColumnAttribute.cs +++ b/src/RepoCleaner/ConsoleComponents/OverviewTableColumnAttribute.cs @@ -1,14 +1,8 @@ namespace Develix.RepoCleaner.ConsoleComponents; [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] -internal class OverviewTableColumnAttribute : Attribute +internal class OverviewTableColumnAttribute(string title, int order) : Attribute { - public string Title { get; } - public int Order { get; } - - public OverviewTableColumnAttribute(string title, int order) - { - Title = title ?? throw new ArgumentNullException(nameof(title)); - Order = order; - } + public string Title { get; } = title ?? throw new ArgumentNullException(nameof(title)); + public int Order { get; } = order; } diff --git a/src/RepoCleaner/ConsoleComponents/OverviewTablePullRequest.cs b/src/RepoCleaner/ConsoleComponents/OverviewTablePullRequest.cs index 7c087ec..2d846d6 100644 --- a/src/RepoCleaner/ConsoleComponents/OverviewTablePullRequest.cs +++ b/src/RepoCleaner/ConsoleComponents/OverviewTablePullRequest.cs @@ -3,15 +3,10 @@ namespace Develix.RepoCleaner.ConsoleComponents; -internal class OverviewTablePullRequest : OverviewTableRowCustomBase +internal class OverviewTablePullRequest(OverviewTableRowBase parentRow, PullRequest pullRequest) + : OverviewTableRowCustomBase(parentRow, ":right_arrow_curving_down:", GetPullRequestTitle(pullRequest)) { - private readonly PullRequest pullRequest; - - public OverviewTablePullRequest(OverviewTableRowBase parentRow, PullRequest pullRequest) - : base(parentRow, ":right_arrow_curving_down:", GetPullRequestTitle(pullRequest)) - { - this.pullRequest = pullRequest; - } + private readonly PullRequest pullRequest = pullRequest; public override IEnumerable GetRowData() { diff --git a/src/RepoCleaner/ConsoleComponents/OverviewTableRow.cs b/src/RepoCleaner/ConsoleComponents/OverviewTableRow.cs index cf8db04..19e9282 100644 --- a/src/RepoCleaner/ConsoleComponents/OverviewTableRow.cs +++ b/src/RepoCleaner/ConsoleComponents/OverviewTableRow.cs @@ -4,35 +4,26 @@ namespace Develix.RepoCleaner.ConsoleComponents; -internal class OverviewTableRow : OverviewTableRowBase +internal class OverviewTableRow(Branch branch, WorkItem? relatedWorkItem, IReadOnlyDictionary workItemTypeIcons) + : OverviewTableRowBase { [OverviewTableColumn("Name", 10)] - public string BranchName { get; } + public string BranchName { get; } = GetBranchName(branch); [OverviewTableColumn("ID", 20)] - public string WorkItemId { get; } + public string WorkItemId { get; } = GetWorkItemId(relatedWorkItem); [OverviewTableColumn(":white_question_mark:", 30)] - public string WorkItemTypeString { get; } + public string WorkItemTypeString { get; } = GetWorkItemType(relatedWorkItem, workItemTypeIcons); [OverviewTableColumn("WI Title", 40)] - public string Title { get; } + public string Title { get; } = GetColoredTitle(relatedWorkItem); [OverviewTableColumn("WI", 50)] - public string WorkItemStatusString { get; } + public string WorkItemStatusString { get; } = GetWorkItemStatus(relatedWorkItem); [OverviewTableColumn(":up_arrow:", 60)] - public string TrackingBranchStatusString { get; } - - public OverviewTableRow(Branch branch, WorkItem? relatedWorkItem, IReadOnlyDictionary workItemTypeIcons) - { - BranchName = GetBranchName(branch); - WorkItemId = GetWorkItemId(relatedWorkItem); - WorkItemTypeString = GetWorkItemType(relatedWorkItem, workItemTypeIcons); - Title = GetColoredTitle(relatedWorkItem); - WorkItemStatusString = GetWorkItemStatus(relatedWorkItem); - TrackingBranchStatusString = GetTrackingBranchStatus(branch); - } + public string TrackingBranchStatusString { get; } = GetTrackingBranchStatus(branch); private static string GetBranchName(Branch branch) { diff --git a/src/RepoCleaner/ConsoleComponents/OverviewTableRowAuthor.cs b/src/RepoCleaner/ConsoleComponents/OverviewTableRowAuthor.cs index ea468a1..4fab7bc 100644 --- a/src/RepoCleaner/ConsoleComponents/OverviewTableRowAuthor.cs +++ b/src/RepoCleaner/ConsoleComponents/OverviewTableRowAuthor.cs @@ -1,9 +1,6 @@ namespace Develix.RepoCleaner.ConsoleComponents; -internal class OverviewTableRowAuthor : OverviewTableRowCustomBase +internal class OverviewTableRowAuthor(OverviewTableRowBase parentRow, string author) + : OverviewTableRowCustomBase(parentRow, ":pencil:", author) { - public OverviewTableRowAuthor(OverviewTableRowBase parentRow, string author) - : base(parentRow, ":pencil:", author) - { - } } diff --git a/src/RepoCleaner/ConsoleComponents/OverviewTableRowBase.cs b/src/RepoCleaner/ConsoleComponents/OverviewTableRowBase.cs index 6f7aabd..85e6ced 100644 --- a/src/RepoCleaner/ConsoleComponents/OverviewTableRowBase.cs +++ b/src/RepoCleaner/ConsoleComponents/OverviewTableRowBase.cs @@ -31,10 +31,8 @@ public virtual IEnumerable GetColumns() private Markup GetMarkup(PropertyInfo property) { - var stringValue = (string?)property.GetValue(this); - if (stringValue is null) - throw new InvalidOperationException($"Could not get the value of string property '{property.Name}' of type '{GetType().Name}'!"); - + var stringValue = (string?)property.GetValue(this) + ?? throw new InvalidOperationException($"Could not get the value of string property '{property.Name}' of type '{GetType().Name}'!"); try { return new(stringValue); diff --git a/src/RepoCleaner/ConsoleComponents/OverviewTableRowCustomBase.cs b/src/RepoCleaner/ConsoleComponents/OverviewTableRowCustomBase.cs index 00a1050..1c9c8fd 100644 --- a/src/RepoCleaner/ConsoleComponents/OverviewTableRowCustomBase.cs +++ b/src/RepoCleaner/ConsoleComponents/OverviewTableRowCustomBase.cs @@ -3,21 +3,13 @@ namespace Develix.RepoCleaner.ConsoleComponents; -internal abstract class OverviewTableRowCustomBase : OverviewTableRowBase +internal abstract class OverviewTableRowCustomBase(OverviewTableRowBase parentRow, string icon, string data) : OverviewTableRowBase { - protected readonly PropertyInfo[] columnPropertyInfos; - protected readonly OverviewTableRowBase parentRow; + protected readonly PropertyInfo[] columnPropertyInfos = GetStringPropertyInfos(parentRow.GetType()).Select(x => x.Property).ToArray(); + protected readonly OverviewTableRowBase parentRow = parentRow; - protected string Icon { get; } - protected string Data { get; } - - public OverviewTableRowCustomBase(OverviewTableRowBase parentRow, string icon, string data) - { - columnPropertyInfos = GetStringPropertyInfos(parentRow.GetType()).Select(x => x.Property).ToArray(); - this.parentRow = parentRow; - Icon = icon; - Data = data; - } + protected string Icon { get; } = icon; + protected string Data { get; } = data; public override IEnumerable GetColumns() => parentRow.GetColumns(); diff --git a/src/RepoCleaner/ConsoleComponents/OverviewTableRowWithProject.cs b/src/RepoCleaner/ConsoleComponents/OverviewTableRowWithProject.cs index bfabce7..27394ad 100644 --- a/src/RepoCleaner/ConsoleComponents/OverviewTableRowWithProject.cs +++ b/src/RepoCleaner/ConsoleComponents/OverviewTableRowWithProject.cs @@ -4,16 +4,15 @@ namespace Develix.RepoCleaner.ConsoleComponents; -internal class OverviewTableRowWithProject : OverviewTableRow +internal class OverviewTableRowWithProject( + Branch branch, + WorkItem? relatedWorkItem, + IReadOnlyDictionary workItemTypeIcons, + IReadOnlyDictionary shortProjectNames) + : OverviewTableRow(branch, relatedWorkItem, workItemTypeIcons) { [OverviewTableColumn("Project", 25)] - public string TeamProject { get; } - - public OverviewTableRowWithProject(Branch branch, WorkItem? relatedWorkItem, IReadOnlyDictionary workItemTypeIcons, IReadOnlyDictionary shortProjectNames) - : base(branch, relatedWorkItem, workItemTypeIcons) - { - TeamProject = GetTeamProject(relatedWorkItem, shortProjectNames); - } + public string TeamProject { get; } = GetTeamProject(relatedWorkItem, shortProjectNames); private static string GetTeamProject(WorkItem? relatedWorkItem, IReadOnlyDictionary shortProjectNames) { diff --git a/src/RepoCleaner/Git/Model/Repository.cs b/src/RepoCleaner/Git/Model/Repository.cs index 465e9de..bc68c3b 100644 --- a/src/RepoCleaner/Git/Model/Repository.cs +++ b/src/RepoCleaner/Git/Model/Repository.cs @@ -4,11 +4,11 @@ public class Repository { public static readonly Repository DefaultInvalid = new("No repository loaded", new Branch()); - public string Name { get; } + public string Name { get; } public Branch CurrentBranch { get; } - private readonly List branches = new(); + private readonly List branches = []; public Repository(string name, Branch currentBranch) { diff --git a/src/RepoCleaner/Git/Reader.cs b/src/RepoCleaner/Git/Reader.cs index 3327784..f075fe0 100644 --- a/src/RepoCleaner/Git/Reader.cs +++ b/src/RepoCleaner/Git/Reader.cs @@ -61,7 +61,7 @@ private static Result GetRepository(string path) Prune = true, CredentialsProvider = handlerResult.Value, }; - repository.Network.Fetch(remote.Name, Enumerable.Empty(), options); + repository.Network.Fetch(remote.Name, [], options); } else { diff --git a/src/RepoCleaner/Model/AppSettings.cs b/src/RepoCleaner/Model/AppSettings.cs index 056a8bf..ca22bae 100644 --- a/src/RepoCleaner/Model/AppSettings.cs +++ b/src/RepoCleaner/Model/AppSettings.cs @@ -1,4 +1,6 @@ -namespace Develix.RepoCleaner.Model; +using Microsoft.Extensions.Configuration; + +namespace Develix.RepoCleaner.Model; public class AppSettings { @@ -10,8 +12,24 @@ public class AppSettings /// A collection of branches to exclude. Use Regex patterns. /// /// Use `^release` to exclude all branches that start with release or `^main$` to exclude the main branch - public List ExcludedBranches { get; set; } = new(); + public List ExcludedBranches { get; set; } = []; + + public Dictionary ShortProjectNames { get; set; } = []; + public Dictionary WorkItemTypeIcons { get; set; } = []; + + public static AppSettings Create() + { + var configurationBuilder = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false) + ; + if (Environment.GetEnvironmentVariable("DEV_ENVIRONMENT") is string env) + configurationBuilder.AddJsonFile($"appsettings.{env}.json", optional: false, reloadOnChange: false); + + var configuration = configurationBuilder.Build(); + var settings = new AppSettings(); + var settingsSection = configuration.GetSection("Settings"); + settingsSection.Bind(settings); - public Dictionary ShortProjectNames { get; set; } = new(); - public Dictionary WorkItemTypeIcons { get; set; } = new(); + return settings; + } } diff --git a/src/RepoCleaner/Model/ConsoleArguments.cs b/src/RepoCleaner/Model/ConsoleArguments.cs deleted file mode 100644 index 7b3743e..0000000 --- a/src/RepoCleaner/Model/ConsoleArguments.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Develix.RepoCleaner.Model; - -public class ConsoleArguments -{ - public bool Delete { get; set; } - public string? Path { get; init; } - public BranchSourceKind Branches { get; init; } - public bool Pr { get; init; } - public bool Author { get; init; } - public bool Config { get; init; } -} diff --git a/src/RepoCleaner/Model/ConsoleArgumentsBinder.cs b/src/RepoCleaner/Model/ConsoleArgumentsBinder.cs deleted file mode 100644 index fd71061..0000000 --- a/src/RepoCleaner/Model/ConsoleArgumentsBinder.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.CommandLine; -using System.CommandLine.Binding; - -namespace Develix.RepoCleaner.Model; - -public class ConsoleArgumentsBinder : BinderBase -{ - private static readonly Option deleteOption = new( - new[] { "--delete", "-d" }, - getDefaultValue: () => false, - description: "Show 'delete branches' prompt."); - - private static readonly Option pathOption = new( - new[] { "--path", "-p" }, - getDefaultValue: () => null, - description: "The path to a local repository."); - - private static readonly Option branchesOption = new( - new[] { "--branch" }, - getDefaultValue: () => BranchSourceKind.Local, - description: "Specifies which branches to include."); - - private static readonly Option pullRequestOption = new( - new[] { "--pr" }, - getDefaultValue: () => false, - description: "Include pull requests when loading work items."); - - private static readonly Option authorOption = new( - new[] { "--author" }, - getDefaultValue: () => false, - description: "Include author and date of the last commit."); - - private static readonly Option configOption = new( - new[] { "--config" }, - getDefaultValue: () => false, - description: "Enter configuration mode and allow to set the needed tokens for azure devops."); - - public static RootCommand GetRootCommand() - { - return new("RepoCleaner - Stuff with repositories and cleaning...") - { - deleteOption, - pathOption, - branchesOption, - pullRequestOption, - authorOption, - configOption - }; - } - - protected override ConsoleArguments GetBoundValue(BindingContext bindingContext) - { - return new ConsoleArguments() - { - Author = bindingContext.ParseResult.GetValueForOption(authorOption), - Branches = bindingContext.ParseResult.GetValueForOption(branchesOption), - Config = bindingContext.ParseResult.GetValueForOption(configOption), - Delete = bindingContext.ParseResult.GetValueForOption(deleteOption), - Path = bindingContext.ParseResult.GetValueForOption(pathOption), - Pr = bindingContext.ParseResult.GetValueForOption(pullRequestOption), - }; - } -} diff --git a/src/RepoCleaner/Program.cs b/src/RepoCleaner/Program.cs index 60d0ea5..884cf7e 100644 --- a/src/RepoCleaner/Program.cs +++ b/src/RepoCleaner/Program.cs @@ -1,13 +1,9 @@ -using System.CommandLine; -using System.CommandLine.Builder; -using System.CommandLine.Help; -using System.CommandLine.Parsing; -using Develix.AzureDevOps.Connector.Service; +using Develix.AzureDevOps.Connector.Service; +using Develix.RepoCleaner.ConsoleComponents.Cli; +using Develix.RepoCleaner.ConsoleComponents.Cli.Infrastructure; using Develix.RepoCleaner.Model; -using Fluxor; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Spectre.Console; +using Spectre.Console.Cli; namespace Develix.RepoCleaner; @@ -15,57 +11,23 @@ public class Program { static async Task Main(string[] args) { - var services = new ServiceCollection(); - services.AddScoped(); - services.AddFluxor(o => o.ScanAssemblies(typeof(Program).Assembly)); - services.AddScoped(); - services.AddScoped(); - - var customBinder = new ConsoleArgumentsBinder(); - var rootCommand = ConsoleArgumentsBinder.GetRootCommand(); - rootCommand.SetHandler((ConsoleArguments cs) => Run(cs, services), customBinder); - - var parser = new CommandLineBuilder(rootCommand) - .UseDefaults() - .UseHelp(ctx => - { - ctx.HelpBuilder.CustomizeLayout( - _ => - HelpBuilder.Default - .GetLayout() - .Skip(1) - .Prepend( - _ => AnsiConsole.Write(new CanvasImage($"{AppContext.BaseDirectory}images/unicorn.png")) - )); - }) - .Build(); - - return await parser.InvokeAsync(args); + var registrar = InitRegistrar(); + var app = new CommandApp(registrar); + app.Configure(configurator => + { + configurator.AddCommand("config"); + }); + return await app.RunAsync(args); } - private static async Task Run(ConsoleArguments consoleArguments, IServiceCollection services) + private static TypeRegistrar InitRegistrar() { - var appSettings = GetAppSettings(); - var serviceProvider = services.BuildServiceProvider(); - var app = serviceProvider.GetRequiredService(); - await app.Run(consoleArguments, appSettings); - } - - private static AppSettings GetAppSettings() - { - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false); - if (Environment.GetEnvironmentVariable("DEV_ENVIRONMENT") is string env) - configurationBuilder.AddJsonFile($"appsettings.{env}.json", optional: false, reloadOnChange: false); - - var configuration = configurationBuilder.Build(); - - var appSettings = new AppSettings(); - var settingsSection = configuration.GetSection(AppSettings.SettingsSection); - settingsSection.Bind(appSettings); - if (appSettings.AzureDevopsOrgUri is null) - throw new InvalidOperationException($"No azure devops uri is set. Please set a valid uri in appsettings.json."); - - return appSettings; + var appSettings = AppSettings.Create(); + var registrations = new ServiceCollection(); + registrations.AddSingleton(appSettings); + registrations.AddScoped(); + registrations.AddScoped(); + var registrar = new TypeRegistrar(registrations); + return registrar; } } diff --git a/src/RepoCleaner/RepoCleaner.csproj b/src/RepoCleaner/RepoCleaner.csproj index 418e877..f1140e9 100644 --- a/src/RepoCleaner/RepoCleaner.csproj +++ b/src/RepoCleaner/RepoCleaner.csproj @@ -9,24 +9,17 @@ Develix.RepoCleaner win-x64 en - 1.5.0 + 1.6.0 - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + diff --git a/src/RepoCleaner/Store/ConsoleSettingsUseCase/ConsoleSettingsState.cs b/src/RepoCleaner/Store/ConsoleSettingsUseCase/ConsoleSettingsState.cs deleted file mode 100644 index 13361b8..0000000 --- a/src/RepoCleaner/Store/ConsoleSettingsUseCase/ConsoleSettingsState.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Develix.Essentials.Core; -using Develix.RepoCleaner.Model; -using Fluxor; - -namespace Develix.RepoCleaner.Store.ConsoleSettingsUseCase; - -[FeatureState] -public record ConsoleSettingsState -{ - public bool ShowDeletePrompt { get; set; } - public string? Path { get; init; } - public BranchSourceKind Branches { get; init; } - public bool ShowPullRequests { get; init; } - public bool ShowLastCommitAuthor { get; init; } - public bool Config { get; init; } - public Uri AzureDevOpsUri { get; init; } = null!; - public List ExcludedBranches { get; init; } = null!; - public IReadOnlyDictionary ShortProjectNames { get; init; } = null!; - public IReadOnlyDictionary WorkItemTypeIcons { get; init; } = null!; - public bool Configuring { get; init; } - public Result ConfigureResult { get; init; } = Result.Fail($"{nameof(ConfigureCredentialsResultAction)} was not executed"); -} diff --git a/src/RepoCleaner/Store/ConsoleSettingsUseCase/Effects.cs b/src/RepoCleaner/Store/ConsoleSettingsUseCase/Effects.cs deleted file mode 100644 index fee52e3..0000000 --- a/src/RepoCleaner/Store/ConsoleSettingsUseCase/Effects.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Develix.CredentialStore.Win32; -using Fluxor; - -namespace Develix.RepoCleaner.Store.ConsoleSettingsUseCase; - -public class Effects -{ - [EffectMethod] - public Task HandleConfigureCredentialsAction(ConfigureCredentialsAction action, IDispatcher dispatcher) - { - var credential = new Credential("token", action.Token, action.CredentialName); - var crudResult = CredentialManager.CreateOrUpdate(credential); - var resultAction = new ConfigureCredentialsResultAction(crudResult); - dispatcher.Dispatch(resultAction); - return Task.CompletedTask; - } -} diff --git a/src/RepoCleaner/Store/ConsoleSettingsUseCase/Reducers.cs b/src/RepoCleaner/Store/ConsoleSettingsUseCase/Reducers.cs deleted file mode 100644 index d1d77a1..0000000 --- a/src/RepoCleaner/Store/ConsoleSettingsUseCase/Reducers.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Fluxor; - -namespace Develix.RepoCleaner.Store.ConsoleSettingsUseCase; - -public static class Reducers -{ - [ReducerMethod] - public static ConsoleSettingsState SetConsoleSettings(ConsoleSettingsState state, SetConsoleSettingsAction action) - { - return state with - { - ShowLastCommitAuthor = action.ConsoleArguments.Author, - AzureDevOpsUri = action.AppSettings.AzureDevopsOrgUri, - Branches = action.ConsoleArguments.Branches, - Config = action.ConsoleArguments.Config, - ExcludedBranches = action.AppSettings.ExcludedBranches, - ShortProjectNames = new Dictionary(action.AppSettings.ShortProjectNames, StringComparer.OrdinalIgnoreCase), - WorkItemTypeIcons = new Dictionary(action.AppSettings.WorkItemTypeIcons, StringComparer.OrdinalIgnoreCase), - Path = action.ConsoleArguments.Path, - ShowPullRequests = action.ConsoleArguments.Pr, - ShowDeletePrompt = action.ConsoleArguments.Delete, - }; - } - - [ReducerMethod(typeof(ConfigureCredentialsAction))] - public static ConsoleSettingsState ExecuteConfigureCredentialsAction(ConsoleSettingsState state) - { - return state with { Configuring = true }; - } - - [ReducerMethod] - public static ConsoleSettingsState ExecuteConfigureCredentialsResultAction(ConsoleSettingsState state, ConfigureCredentialsResultAction action) - { - return state with { Configuring = false, ConfigureResult = action.Result }; - } -} diff --git a/src/RepoCleaner/Store/ConsoleSettingsUseCaseActions.cs b/src/RepoCleaner/Store/ConsoleSettingsUseCaseActions.cs deleted file mode 100644 index 3e58ae0..0000000 --- a/src/RepoCleaner/Store/ConsoleSettingsUseCaseActions.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Develix.Essentials.Core; -using Develix.RepoCleaner.Model; - -namespace Develix.RepoCleaner.Store; - -public record SetConsoleSettingsAction(ConsoleArguments ConsoleArguments, AppSettings AppSettings); - -public record ConfigureCredentialsAction(string CredentialName, string Token); - -public record ConfigureCredentialsResultAction(Result Result); diff --git a/src/RepoCleaner/Store/GetRepositoryInfoUseCaseActions.cs b/src/RepoCleaner/Store/GetRepositoryInfoUseCaseActions.cs deleted file mode 100644 index 3b77899..0000000 --- a/src/RepoCleaner/Store/GetRepositoryInfoUseCaseActions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Develix.AzureDevOps.Connector.Model; -using Develix.Essentials.Core; -using Develix.RepoCleaner.Git.Model; -using Develix.RepoCleaner.Model; - -namespace Develix.RepoCleaner.Store; - -public record InitRepositoryAction(BranchSourceKind BranchSourceKind); - -public record LoginServicesAction(string CredentialName); - -public record LoginReposServiceAction(string CredentialName); - -public record LoginReposServiceResultAction(Result LoginResult); - -public record LoginWorkItemServiceAction(string CredentialName); - -public record LoginWorkItemServiceResultAction(Result LoginResult); - -public record SetRepositoryAction(Repository Repository); - -public record SetWorkItemsAction(IReadOnlyList WorkItems); - -public record AddErrorAction(string Message); diff --git a/src/RepoCleaner/Store/RepositoryInfoUseCase/Effects.cs b/src/RepoCleaner/Store/RepositoryInfoUseCase/Effects.cs deleted file mode 100644 index 8806efb..0000000 --- a/src/RepoCleaner/Store/RepositoryInfoUseCase/Effects.cs +++ /dev/null @@ -1,137 +0,0 @@ -using Develix.AzureDevOps.Connector.Model; -using Develix.AzureDevOps.Connector.Service; -using Develix.CredentialStore.Win32; -using Develix.RepoCleaner.Git; -using Develix.RepoCleaner.Model; -using Develix.RepoCleaner.Store.ConsoleSettingsUseCase; -using Fluxor; - -namespace Develix.RepoCleaner.Store.RepositoryInfoUseCase; - -public class Effects -{ - private readonly IState consoleSettingsState; - private readonly IState repositoryInfoState; - private readonly IWorkItemService workItemService; - private readonly IReposService reposService; - - public Effects( - IState consoleSettingsState, - IState repositoryInfoState, - IWorkItemService workItemService, - IReposService reposService) - { - this.consoleSettingsState = consoleSettingsState; - this.repositoryInfoState = repositoryInfoState; - this.workItemService = workItemService; - this.reposService = reposService; - } - - [EffectMethod] - public Task HandleLoginServicesAction(LoginServicesAction action, IDispatcher dispatcher) - { - dispatcher.Dispatch(new LoginWorkItemServiceAction(action.CredentialName)); - if (consoleSettingsState.Value.ShowPullRequests) - dispatcher.Dispatch(new LoginReposServiceAction(action.CredentialName)); - - return Task.CompletedTask; - } - - [EffectMethod] - public async Task HandleLoginWorkItemServiceAction(LoginWorkItemServiceAction action, IDispatcher dispatcher) - { - var credential = CredentialManager.Get(action.CredentialName); - var result = await workItemService.Initialize(consoleSettingsState.Value.AzureDevOpsUri, credential.Value.Password!); - dispatcher.Dispatch(new LoginWorkItemServiceResultAction(result)); - } - - [EffectMethod] - public Task HandleLoginWorkItemServiceResultAction(LoginWorkItemServiceResultAction action, IDispatcher dispatcher) - { - if (!action.LoginResult.Valid) - { - var errorMessage = $"[red]Could not login work item service.[/] " + - $"Uri: [grey30]{consoleSettingsState.Value.AzureDevOpsUri}[/] | " + - $"Error: [grey30]{action.LoginResult.Message}[/]"; - AddErrorMessage(errorMessage, dispatcher); - } - - return Task.CompletedTask; - } - - [EffectMethod] - public async Task HandleLoginRepoServiceAction(LoginReposServiceAction action, IDispatcher dispatcher) - { - var credential = CredentialManager.Get(action.CredentialName); - var result = await reposService.Initialize(consoleSettingsState.Value.AzureDevOpsUri, credential.Value.Password!); - dispatcher.Dispatch(new LoginReposServiceResultAction(result)); - } - - [EffectMethod] - public Task HandleLoginRepoServiceResultAction(LoginReposServiceResultAction action, IDispatcher dispatcher) - { - if (!action.LoginResult.Valid) - { - var errorMessage = $"[red]Could not login repo service.[/]" + - $"Uri: [grey30]{consoleSettingsState.Value.AzureDevOpsUri}[/] | " + - $"Error: [grey30]{action.LoginResult.Message}[/]"; - AddErrorMessage(errorMessage, dispatcher); - } - - return Task.CompletedTask; - } - - [EffectMethod] - public Task HandleInitRepositoryAction(InitRepositoryAction action, IDispatcher dispatcher) - { - var path = consoleSettingsState.Value.Path ?? Directory.GetCurrentDirectory(); - var repositoryResult = action.BranchSourceKind switch - { - BranchSourceKind.Local => Reader.GetLocalRepo(path, consoleSettingsState.Value.ExcludedBranches), - BranchSourceKind.Remote => Reader.GetRemoteRepo(path, consoleSettingsState.Value.ExcludedBranches), - BranchSourceKind.All => Reader.GetRepo(path, consoleSettingsState.Value.ExcludedBranches), - _ => throw new NotSupportedException($"The {nameof(BranchSourceKind)} '{action.BranchSourceKind}' is not supported!"), - }; - if (!repositoryResult.Valid) - AddErrorMessage($"[red]Failed to init repository![/] Error: [grey30]{repositoryResult.Message}[/]", dispatcher); - - var repository = repositoryResult.Valid - ? repositoryResult.Value - : Git.Model.Repository.DefaultInvalid; - - var setRepositoryAction = new SetRepositoryAction(repository); - dispatcher.Dispatch(setRepositoryAction); - - return Task.CompletedTask; - } - - [EffectMethod] - public async Task HandleSetRepositoryAction(SetRepositoryAction action, IDispatcher dispatcher) - { - if (repositoryInfoState.Value.WorkItemServiceState != ServiceConnectionState.Connected) - { - Dispatch(Array.Empty()); - return; - } - - var ids = action.Repository.Branches.Select(b => b.RelatedWorkItemId).OfType(); - var workItemsResult = await workItemService.GetWorkItems(ids, consoleSettingsState.Value.ShowPullRequests); - - if (workItemsResult.Valid) - Dispatch(workItemsResult.Value); - else - Dispatch(Array.Empty()); - - void Dispatch(IReadOnlyList workItems) - { - var setWorkItemsAction = new SetWorkItemsAction(workItems); - dispatcher.Dispatch(setWorkItemsAction); - } - } - - private void AddErrorMessage(string errorMessage, IDispatcher dispatcher) - { - var addErrorAction = new AddErrorAction(errorMessage); - dispatcher.Dispatch(addErrorAction); - } -} diff --git a/src/RepoCleaner/Store/RepositoryInfoUseCase/Reducers.cs b/src/RepoCleaner/Store/RepositoryInfoUseCase/Reducers.cs deleted file mode 100644 index 6c671c2..0000000 --- a/src/RepoCleaner/Store/RepositoryInfoUseCase/Reducers.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Fluxor; - -namespace Develix.RepoCleaner.Store.RepositoryInfoUseCase; - -public static class Reducers -{ - [ReducerMethod(typeof(InitRepositoryAction))] - public static RepositoryInfoState InitRepository(RepositoryInfoState state) - { - return state with { WorkItemsLoaded = false, RepositoryLoaded = false }; - } - - [ReducerMethod(typeof(LoginServicesAction))] - public static RepositoryInfoState Login(RepositoryInfoState state) - { - return state with { }; - } - - [ReducerMethod(typeof(LoginReposServiceAction))] - public static RepositoryInfoState LoginReposService(RepositoryInfoState state) - { - return state with { ReposServiceConnected = false }; - } - - [ReducerMethod] - public static RepositoryInfoState LoginReposService(RepositoryInfoState state, LoginReposServiceResultAction action) - { - return state with { ReposServiceConnected = action.LoginResult.Valid }; - } - - [ReducerMethod(typeof(LoginWorkItemServiceAction))] - public static RepositoryInfoState LoginWorkItemService(RepositoryInfoState state) - { - return state with { WorkItemServiceState = ServiceConnectionState.Connecting }; - } - - [ReducerMethod] - public static RepositoryInfoState LoginRepoService(RepositoryInfoState state, LoginWorkItemServiceResultAction action) - { - var serviceState = action.LoginResult.Valid ? ServiceConnectionState.Connected : ServiceConnectionState.FailedToConnect; - return state with { WorkItemServiceState = serviceState }; - } - - [ReducerMethod] - public static RepositoryInfoState SetRepository(RepositoryInfoState state, SetRepositoryAction action) - { - return state with { Repository = action.Repository, RepositoryLoaded = true }; - } - - [ReducerMethod] - public static RepositoryInfoState SetWorkItems(RepositoryInfoState state, SetWorkItemsAction action) - { - return state with { WorkItems = action.WorkItems, WorkItemsLoaded = true }; - } - - [ReducerMethod] - public static RepositoryInfoState AddErrorMessage(RepositoryInfoState state, AddErrorAction action) - { - return state with { ErrorMessages = state.ErrorMessages.Append(action.Message).ToList() }; - } -} diff --git a/src/RepoCleaner/Store/RepositoryInfoUseCase/RepositoryInfoState.cs b/src/RepoCleaner/Store/RepositoryInfoUseCase/RepositoryInfoState.cs deleted file mode 100644 index e66bdd9..0000000 --- a/src/RepoCleaner/Store/RepositoryInfoUseCase/RepositoryInfoState.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Develix.AzureDevOps.Connector.Model; -using Develix.RepoCleaner.Git.Model; -using Fluxor; - -namespace Develix.RepoCleaner.Store.RepositoryInfoUseCase; - -[FeatureState] -public record RepositoryInfoState -{ - public bool RepositoryLoaded { get; init; } - public bool WorkItemsLoaded { get; init; } - public bool ReposServiceConnected { get; init; } - public ServiceConnectionState WorkItemServiceState { get; init; } = ServiceConnectionState.Disconnected; - public Repository Repository { get; init; } = Repository.DefaultInvalid; - - public IReadOnlyList WorkItems { get; init; } = new List(); - public IReadOnlyList ErrorMessages { get; set; } = new List(); -} diff --git a/src/RepoCleaner/Store/ServiceConnectionState.cs b/src/RepoCleaner/Store/ServiceConnectionState.cs deleted file mode 100644 index 2aad2a4..0000000 --- a/src/RepoCleaner/Store/ServiceConnectionState.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Develix.RepoCleaner.Store; - -public enum ServiceConnectionState -{ - Invalid = 0, - Disconnected, - Connecting, - Connected, - FailedToConnect, -}