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;
|
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
|
public interface IGitRepository : IDisposable
|
||||||
{
|
{
|
||||||
Task ForceCheckout(string branch);
|
Task ForceCheckout(CancellationToken token, string branch);
|
||||||
Task Fetch(string remote = "origin");
|
Task Fetch(CancellationToken token, string remote = "origin");
|
||||||
Task ResetHard(string toBranchOrSha1);
|
Task ResetHard(CancellationToken token, string toBranchOrSha1);
|
||||||
Task SetRemote(string name, Uri newUrl);
|
Task SetRemote(CancellationToken token, string name, Uri newUrl);
|
||||||
Task Clone(Uri cloneUrl, string? branch = null, int depth = 0);
|
Task Clone(CancellationToken token, Uri cloneUrl, string? branch = null, int depth = 0);
|
||||||
Task Status();
|
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