Repo updating is also a little more robust now.json-serializing-nullable-fields-issue
parent
b5c49d81c5
commit
ef8ae7dd48
@ -0,0 +1,41 @@
|
||||
namespace Recyclarr.Cli.Console.Helpers;
|
||||
|
||||
// Taken from: https://github.com/spectreconsole/spectre.console/issues/701#issuecomment-1081834778
|
||||
internal sealed class ConsoleAppCancellationTokenSource
|
||||
{
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
public CancellationToken Token => _cts.Token;
|
||||
|
||||
public ConsoleAppCancellationTokenSource()
|
||||
{
|
||||
System.Console.CancelKeyPress += OnCancelKeyPress;
|
||||
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
|
||||
|
||||
using var _ = _cts.Token.Register(() =>
|
||||
{
|
||||
AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
|
||||
System.Console.CancelKeyPress -= OnCancelKeyPress;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e)
|
||||
{
|
||||
// NOTE: cancel event, don't terminate the process
|
||||
e.Cancel = true;
|
||||
|
||||
_cts.Cancel();
|
||||
}
|
||||
|
||||
private void OnProcessExit(object? sender, EventArgs e)
|
||||
{
|
||||
if (_cts.IsCancellationRequested)
|
||||
{
|
||||
// NOTE: SIGINT (cancel key was pressed, this shouldn't ever actually hit however, as we remove the event
|
||||
// handler upon cancellation of the `cancellationSource`)
|
||||
return;
|
||||
}
|
||||
|
||||
_cts.Cancel();
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
using System.IO.Abstractions;
|
||||
|
||||
namespace Recyclarr.Common;
|
||||
|
||||
public class FileUtilities : IFileUtilities
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public FileUtilities(IFileSystem fileSystem)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
public void DeleteReadOnlyDirectory(string directory)
|
||||
{
|
||||
if (!_fileSystem.Directory.Exists(directory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var subdirectory in _fileSystem.Directory.EnumerateDirectories(directory))
|
||||
{
|
||||
DeleteReadOnlyDirectory(subdirectory);
|
||||
}
|
||||
|
||||
foreach (var fileName in Directory.EnumerateFiles(directory))
|
||||
{
|
||||
var fileInfo = _fileSystem.FileInfo.New(fileName);
|
||||
fileInfo.Attributes = FileAttributes.Normal;
|
||||
fileInfo.Delete();
|
||||
}
|
||||
|
||||
_fileSystem.Directory.Delete(directory);
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
namespace Recyclarr.Common;
|
||||
|
||||
public interface IFileUtilities
|
||||
{
|
||||
void DeleteReadOnlyDirectory(string directory);
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
using Spectre.Console;
|
||||
|
||||
namespace Recyclarr.TrashLib.Repo;
|
||||
|
||||
public class ConsoleMultiRepoUpdater : IMultiRepoUpdater
|
||||
{
|
||||
private readonly IAnsiConsole _console;
|
||||
private readonly IReadOnlyCollection<IUpdateableRepo> _repos;
|
||||
|
||||
public ConsoleMultiRepoUpdater(IAnsiConsole console, IReadOnlyCollection<IUpdateableRepo> repos)
|
||||
{
|
||||
_console = console;
|
||||
_repos = repos;
|
||||
}
|
||||
|
||||
public async Task UpdateAllRepositories(CancellationToken token)
|
||||
{
|
||||
var options = new ParallelOptions
|
||||
{
|
||||
CancellationToken = token,
|
||||
MaxDegreeOfParallelism = 3
|
||||
};
|
||||
|
||||
await _console.Status().StartAsync("Updating Git Repositories...", async _ =>
|
||||
{
|
||||
await Parallel.ForEachAsync(_repos, options, async (repo, innerToken) => await repo.Update(innerToken));
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace Recyclarr.TrashLib.Repo;
|
||||
|
||||
public interface IMultiRepoUpdater
|
||||
{
|
||||
Task UpdateAllRepositories(CancellationToken token);
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace Recyclarr.TrashLib.Repo;
|
||||
|
||||
public interface IUpdateableRepo
|
||||
{
|
||||
Task Update(CancellationToken token);
|
||||
}
|
@ -1,11 +1,15 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Recyclarr.TrashLib.Repo.VersionControl;
|
||||
|
||||
[SuppressMessage("Design", "CA1068:CancellationToken parameters must come last", Justification =
|
||||
"Doesn't mix well with `params` (which has to be at the end)")]
|
||||
public interface IGitRepository : IDisposable
|
||||
{
|
||||
Task ForceCheckout(string branch);
|
||||
Task Fetch(string remote = "origin");
|
||||
Task ResetHard(string toBranchOrSha1);
|
||||
Task SetRemote(string name, Uri newUrl);
|
||||
Task Clone(Uri cloneUrl, string? branch = null, int depth = 0);
|
||||
Task Status();
|
||||
Task ForceCheckout(CancellationToken token, string branch);
|
||||
Task Fetch(CancellationToken token, string remote = "origin");
|
||||
Task ResetHard(CancellationToken token, string toBranchOrSha1);
|
||||
Task SetRemote(CancellationToken token, string name, Uri newUrl);
|
||||
Task Clone(CancellationToken token, Uri cloneUrl, string? branch = null, int depth = 0);
|
||||
Task Status(CancellationToken token);
|
||||
}
|
||||
|
@ -0,0 +1,40 @@
|
||||
using Recyclarr.Cli.Console.Commands;
|
||||
using Recyclarr.Cli.TestLibrary;
|
||||
using Recyclarr.TrashLib.Repo;
|
||||
|
||||
namespace Recyclarr.Cli.Tests.Console.Commands;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class ListCommandsIntegrationTest : CliIntegrationFixture
|
||||
{
|
||||
[Test, AutoMockData]
|
||||
public async Task Repo_update_is_called_on_list_custom_formats(
|
||||
[Frozen] IMultiRepoUpdater updater,
|
||||
ListCustomFormatsCommand sut)
|
||||
{
|
||||
await sut.ExecuteAsync(default!, new ListCustomFormatsCommand.CliSettings());
|
||||
|
||||
await updater.ReceivedWithAnyArgs().UpdateAllRepositories(default);
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public async Task Repo_update_is_called_on_list_qualities(
|
||||
[Frozen] IMultiRepoUpdater updater,
|
||||
ListQualitiesCommand sut)
|
||||
{
|
||||
await sut.ExecuteAsync(default!, new ListQualitiesCommand.CliSettings());
|
||||
|
||||
await updater.ReceivedWithAnyArgs().UpdateAllRepositories(default);
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public async Task Repo_update_is_called_on_list_release_profiles(
|
||||
[Frozen] IMultiRepoUpdater updater,
|
||||
ListReleaseProfilesCommand sut)
|
||||
{
|
||||
await sut.ExecuteAsync(default!, new ListReleaseProfilesCommand.CliSettings());
|
||||
|
||||
await updater.ReceivedWithAnyArgs().UpdateAllRepositories(default);
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
using System.IO.Abstractions;
|
||||
using Recyclarr.Cli.Console.Settings;
|
||||
using Recyclarr.Cli.Processors.Config;
|
||||
using Recyclarr.Cli.TestLibrary;
|
||||
using Recyclarr.TrashLib.Repo;
|
||||
|
||||
namespace Recyclarr.Cli.Tests.Processors.Config;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class TemplateConfigCreatorIntegrationTest : CliIntegrationFixture
|
||||
{
|
||||
[Test]
|
||||
public void Template_id_matching_works()
|
||||
{
|
||||
const string templatesJson =
|
||||
"""
|
||||
{
|
||||
"radarr": [
|
||||
{
|
||||
"template": "template-file1.yml",
|
||||
"id": "template1"
|
||||
}
|
||||
],
|
||||
"sonarr": [
|
||||
{
|
||||
"template": "template-file2.yml",
|
||||
"id": "template2"
|
||||
},
|
||||
{
|
||||
"template": "template-file3.yml",
|
||||
"id": "template3"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var repo = Resolve<IConfigTemplatesRepo>();
|
||||
Fs.AddFile(repo.Path.File("templates.json"), new MockFileData(templatesJson));
|
||||
Fs.AddEmptyFile(repo.Path.File("template-file1.yml"));
|
||||
Fs.AddEmptyFile(repo.Path.File("template-file2.yml"));
|
||||
// This one shouldn't show up in the result because the user didn't ask for it
|
||||
Fs.AddEmptyFile(repo.Path.File("template-file3.yml"));
|
||||
|
||||
var settings = Substitute.For<ICreateConfigSettings>();
|
||||
settings.Templates.Returns(new[]
|
||||
{
|
||||
"template1",
|
||||
"template2",
|
||||
// This one shouldn't show up in the results because:
|
||||
// User specified it, but no template file exists for it.
|
||||
"template4"
|
||||
});
|
||||
|
||||
var sut = Resolve<TemplateConfigCreator>();
|
||||
sut.Create(settings);
|
||||
|
||||
Fs.AllFiles.Should().Contain(new[]
|
||||
{
|
||||
Paths.ConfigsDirectory.File("template-file1.yml").FullName,
|
||||
Paths.ConfigsDirectory.File("template-file2.yml").FullName
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in new issue