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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions KtsuTools.Merge/MergeBatchService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) ktsu.dev
// All rights reserved.
// Licensed under the MIT license.

namespace KtsuTools.Merge;

using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using KtsuTools.Core.Services.Settings;

public class MergeBatchService(ISettingsService settingsService)
{
private readonly ISettingsService _settings = settingsService;
private MergeBatchSettings? _store;

public IReadOnlyDictionary<string, MergeBatchEntry> List() => GetStore().Batches;

public MergeBatchEntry? Get(string name) =>
GetStore().Batches.TryGetValue(name, out MergeBatchEntry? entry) ? entry : null;

public async Task SaveAsync(string name, MergeBatchEntry entry, CancellationToken ct = default)
{
Ensure.NotNull(name);
Ensure.NotNull(entry);
MergeBatchSettings store = GetStore();
store.Batches[name] = entry;
await _settings.SaveAsync(store).ConfigureAwait(false);
}

public async Task<bool> DeleteAsync(string name, CancellationToken ct = default)
{
Ensure.NotNull(name);
MergeBatchSettings store = GetStore();
if (!store.Batches.Remove(name))
{
return false;
}

await _settings.SaveAsync(store).ConfigureAwait(false);
return true;
}

private MergeBatchSettings GetStore() => _store ??= _settings.LoadOrCreate<MergeBatchSettings>();
}
20 changes: 20 additions & 0 deletions KtsuTools.Merge/MergeBatchSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) ktsu.dev
// All rights reserved.
// Licensed under the MIT license.

namespace KtsuTools.Merge;

using System.Collections.Generic;
using ktsu.AppDataStorage;

public sealed record MergeBatchEntry
{
public required string Directory { get; init; }
public required string Filename { get; init; }
public string? DiffStyle { get; init; }
}

public class MergeBatchSettings : AppData<MergeBatchSettings>
{
public Dictionary<string, MergeBatchEntry> Batches { get; init; } = [];
}
87 changes: 87 additions & 0 deletions KtsuTools.Test/MergeBatchServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) ktsu.dev
// All rights reserved.
// Licensed under the MIT license.

namespace KtsuTools.Test;

using KtsuTools.Core.Services.Settings;
using KtsuTools.Merge;
using Moq;

[TestClass]
public class MergeBatchServiceTests
{
private static (MergeBatchService Service, MergeBatchSettings Store, Mock<ISettingsService> SettingsMock) BuildService()
{
MergeBatchSettings store = new();
Mock<ISettingsService> settings = new();
settings.Setup(s => s.LoadOrCreate<MergeBatchSettings>()).Returns(store);
settings.Setup(s => s.SaveAsync(It.IsAny<MergeBatchSettings>())).Returns(Task.CompletedTask);
MergeBatchService service = new(settings.Object);
return (service, store, settings);
}

[TestMethod]
public async Task SaveListShowDeleteRoundTrip()
{
(MergeBatchService service, MergeBatchSettings store, Mock<ISettingsService> settings) = BuildService();

MergeBatchEntry entry = new()
{
Directory = "/tmp/repos",
Filename = "*.yml",
DiffStyle = "side-by-side",
};

await service.SaveAsync("ci-yaml", entry).ConfigureAwait(false);

Assert.AreEqual(1, service.List().Count);
Assert.IsTrue(service.List().ContainsKey("ci-yaml"));

MergeBatchEntry? loaded = service.Get("ci-yaml");
Assert.IsNotNull(loaded);
Assert.AreEqual("/tmp/repos", loaded.Directory);
Assert.AreEqual("*.yml", loaded.Filename);
Assert.AreEqual("side-by-side", loaded.DiffStyle);

bool removed = await service.DeleteAsync("ci-yaml").ConfigureAwait(false);
Assert.IsTrue(removed);
Assert.AreEqual(0, service.List().Count);
Assert.IsNull(service.Get("ci-yaml"));

settings.Verify(s => s.SaveAsync(store), Times.Exactly(2));
}

[TestMethod]
public async Task DeleteUnknownReturnsFalseAndDoesNotPersist()
{
(MergeBatchService service, _, Mock<ISettingsService> settings) = BuildService();

bool removed = await service.DeleteAsync("missing").ConfigureAwait(false);

Assert.IsFalse(removed);
settings.Verify(s => s.SaveAsync(It.IsAny<MergeBatchSettings>()), Times.Never);
}

[TestMethod]
public void GetUnknownReturnsNull()
{
(MergeBatchService service, _, _) = BuildService();
Assert.IsNull(service.Get("nope"));
}

[TestMethod]
public async Task SaveOverwritesExistingEntry()
{
(MergeBatchService service, _, _) = BuildService();

await service.SaveAsync("name", new MergeBatchEntry { Directory = "a", Filename = "b" }).ConfigureAwait(false);
await service.SaveAsync("name", new MergeBatchEntry { Directory = "c", Filename = "d", DiffStyle = "git" }).ConfigureAwait(false);

MergeBatchEntry? loaded = service.Get("name");
Assert.IsNotNull(loaded);
Assert.AreEqual("c", loaded.Directory);
Assert.AreEqual("d", loaded.Filename);
Assert.AreEqual("git", loaded.DiffStyle);
}
}
39 changes: 39 additions & 0 deletions KtsuTools/Commands/MergeBatchDeleteCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) ktsu.dev
// All rights reserved.
// Licensed under the MIT license.

namespace KtsuTools.Commands;

using System.ComponentModel;
using KtsuTools.Core.UI;
using KtsuTools.Merge;
using Spectre.Console;
using Spectre.Console.Cli;

public sealed class MergeBatchDeleteCommand(MergeBatchService batchService) : AsyncCommand<MergeBatchDeleteCommand.Settings>
{
private readonly MergeBatchService batchService = batchService;

public sealed class Settings : CommandSettings
{
[CommandArgument(0, "<name>")]
[Description("Name of the batch to delete")]
public required string Name { get; init; }
}

public override async Task<int> ExecuteAsync(CommandContext context, Settings settings)
{
Ensure.NotNull(settings);
using CtrlCScope scope = new();

bool removed = await batchService.DeleteAsync(settings.Name, scope.Token).ConfigureAwait(false);
if (!removed)
{
AnsiConsole.MarkupLine($"[red]No batch named '{settings.Name.EscapeMarkup()}'.[/]");
return 1;
}

AnsiConsole.MarkupLine($"[green]Deleted batch '{settings.Name.EscapeMarkup()}'.[/]");
return 0;
}
}
48 changes: 48 additions & 0 deletions KtsuTools/Commands/MergeBatchListCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) ktsu.dev
// All rights reserved.
// Licensed under the MIT license.

namespace KtsuTools.Commands;

using System.Collections.Generic;
using KtsuTools.Merge;
using Spectre.Console;
using Spectre.Console.Cli;

public sealed class MergeBatchListCommand(MergeBatchService batchService) : Command<MergeBatchListCommand.Settings>
{
private readonly MergeBatchService batchService = batchService;

public sealed class Settings : CommandSettings
{
}

public override int Execute(CommandContext context, Settings settings)
{
IReadOnlyDictionary<string, MergeBatchEntry> batches = batchService.List();

if (batches.Count == 0)
{
AnsiConsole.MarkupLine("[dim]No saved batches. Use 'merge-batch save <name> <directory> <filename>' to create one.[/]");
return 0;
}

Table table = new();
table.AddColumn("Name");
table.AddColumn("Directory");
table.AddColumn("Filename");
table.AddColumn("Diff Style");

foreach (KeyValuePair<string, MergeBatchEntry> kvp in batches)
{
table.AddRow(
kvp.Key.EscapeMarkup(),
kvp.Value.Directory.EscapeMarkup(),
kvp.Value.Filename.EscapeMarkup(),
(kvp.Value.DiffStyle ?? "(default)").EscapeMarkup());
}

AnsiConsole.Write(table);
return 0;
}
}
52 changes: 52 additions & 0 deletions KtsuTools/Commands/MergeBatchSaveCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) ktsu.dev
// All rights reserved.
// Licensed under the MIT license.

namespace KtsuTools.Commands;

using System.ComponentModel;
using KtsuTools.Core.UI;
using KtsuTools.Merge;
using Spectre.Console;
using Spectre.Console.Cli;

public sealed class MergeBatchSaveCommand(MergeBatchService batchService) : AsyncCommand<MergeBatchSaveCommand.Settings>
{
private readonly MergeBatchService batchService = batchService;

public sealed class Settings : CommandSettings
{
[CommandArgument(0, "<name>")]
[Description("Name to save this batch under")]
public required string Name { get; init; }

[CommandArgument(1, "<directory>")]
[Description("Directory containing files to merge")]
public required string Directory { get; init; }

[CommandArgument(2, "<filename>")]
[Description("Filename pattern to merge")]
public required string Filename { get; init; }

[CommandOption("--diff-style <STYLE>")]
[Description("Diff style to use when running this batch (reserved for future use)")]
public string? DiffStyle { get; init; }
}

public override async Task<int> ExecuteAsync(CommandContext context, Settings settings)
{
Ensure.NotNull(settings);
using CtrlCScope scope = new();

MergeBatchEntry entry = new()
{
Directory = settings.Directory,
Filename = settings.Filename,
DiffStyle = settings.DiffStyle,
};

await batchService.SaveAsync(settings.Name, entry, scope.Token).ConfigureAwait(false);
AnsiConsole.MarkupLine($"[green]Saved batch '{settings.Name.EscapeMarkup()}'.[/]");
return 0;
}
}
40 changes: 40 additions & 0 deletions KtsuTools/Commands/MergeBatchShowCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) ktsu.dev
// All rights reserved.
// Licensed under the MIT license.

namespace KtsuTools.Commands;

using System.ComponentModel;
using KtsuTools.Merge;
using Spectre.Console;
using Spectre.Console.Cli;

public sealed class MergeBatchShowCommand(MergeBatchService batchService) : Command<MergeBatchShowCommand.Settings>
{
private readonly MergeBatchService batchService = batchService;

public sealed class Settings : CommandSettings
{
[CommandArgument(0, "<name>")]
[Description("Name of the batch to show")]
public required string Name { get; init; }
}

public override int Execute(CommandContext context, Settings settings)
{
Ensure.NotNull(settings);
MergeBatchEntry? entry = batchService.Get(settings.Name);

if (entry is null)
{
AnsiConsole.MarkupLine($"[red]No batch named '{settings.Name.EscapeMarkup()}'.[/]");
return 1;
}

AnsiConsole.MarkupLine($"[bold]Batch:[/] {settings.Name.EscapeMarkup()}");
AnsiConsole.MarkupLine($" [dim]Directory:[/] {entry.Directory.EscapeMarkup()}");
AnsiConsole.MarkupLine($" [dim]Filename:[/] {entry.Filename.EscapeMarkup()}");
AnsiConsole.MarkupLine($" [dim]Diff style:[/] {(entry.DiffStyle ?? "(default)").EscapeMarkup()}");
return 0;
}
}
Loading
Loading