Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Spectre.Cli instead of System.CommandLine #30

Merged
merged 13 commits into from
May 4, 2024
6 changes: 1 addition & 5 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,10 @@
<ItemGroup>
<PackageVersion Include="Develix.AzureDevOps.Connector" Version="1.4.0" />
<PackageVersion Include="Develix.CredentialStore.Win32" Version="1.1.0" />
<PackageVersion Include="Fluxor" Version="5.9.1" />
<PackageVersion Include="LibGit2Sharp" Version="0.30.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Spectre.Console" Version="0.49.1" />
<PackageVersion Include="Spectre.Console.Analyzer" Version="0.49.1" />
<PackageVersion Include="Spectre.Console.ImageSharp" Version="0.49.1" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="Spectre.Console.Cli" Version="0.49.1" />
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 |

Expand Down
79 changes: 0 additions & 79 deletions src/RepoCleaner/App.cs

This file was deleted.

43 changes: 43 additions & 0 deletions src/RepoCleaner/ConsoleComponents/Cli/AzdoClient.cs
Original file line number Diff line number Diff line change
@@ -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<Result> Login(
IReposService reposService,
IWorkItemService workItemService,
RepoCleanerSettings settings,
AppSettings appSettings)
{
IEnumerable<Task<Result>> 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<Result> 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<Result> LoginReposService(IReposService reposService, Uri azureDevopsUri)
{
var credential = CredentialManager.Get(CredentialName);
return credential.Valid
? await reposService.Initialize(azureDevopsUri, credential.Value.Password!)
: Result.Fail(credential.Message);
}
}
31 changes: 31 additions & 0 deletions src/RepoCleaner/ConsoleComponents/Cli/ConfigCommand.cs
Original file line number Diff line number Diff line change
@@ -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<string>("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;
}
}
Original file line number Diff line number Diff line change
@@ -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<object> func)
{
ArgumentNullException.ThrowIfNull(func);
builder.AddSingleton(service, (provider) => func());
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
74 changes: 74 additions & 0 deletions src/RepoCleaner/ConsoleComponents/Cli/RepoCleanerCommand.cs
Original file line number Diff line number Diff line change
@@ -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<RepoCleanerSettings>
{
private readonly AppSettings appSettings = appSettings;
private readonly IReposService reposService = reposService;
private readonly IWorkItemService workItemService = workItemService;

public override async Task<int> 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<WorkItem> 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;
}
}
22 changes: 22 additions & 0 deletions src/RepoCleaner/ConsoleComponents/Cli/RepoCleanerSettings.cs
Original file line number Diff line number Diff line change
@@ -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 <PATH>")]
public string? Path { get; set; }

[CommandOption("-b|--branch <BRANCH_SOURCE_KIND>")]
public BranchSourceKind BranchSource { get; set; } = BranchSourceKind.Local;

[CommandOption("--pr|--pull-requests")]
public bool IncludePullRequests { get; set; }

[CommandOption("--author")]
public bool IncludeAuthor { get; set; }
}
26 changes: 26 additions & 0 deletions src/RepoCleaner/ConsoleComponents/Cli/RepositoryInfo.cs
Original file line number Diff line number Diff line change
@@ -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<Repository> 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<int> GetRelatedWorkItemIds(Repository repository)
{
return repository.Branches.Select(b => b.RelatedWorkItemId).OfType<int>().ToList();
}
}
Loading