Code for cloning the repository refactored to handle failures better. Namely this means deleting the repository and cloning it again if there is a failure.pull/47/head
parent
4869bae216
commit
2fd1601619
@ -1,9 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TrashLib.Radarr.CustomFormat.Guide;
|
||||
|
||||
public interface IRadarrGuideService
|
||||
{
|
||||
Task<IEnumerable<string>> GetCustomFormatJsonAsync();
|
||||
IEnumerable<string> GetCustomFormatJson();
|
||||
}
|
||||
|
@ -1,82 +1,86 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Common;
|
||||
using LibGit2Sharp;
|
||||
using Serilog;
|
||||
using TrashLib.Config.Settings;
|
||||
using TrashLib.Radarr.Config;
|
||||
using VersionControl;
|
||||
|
||||
namespace TrashLib.Radarr.CustomFormat.Guide
|
||||
{
|
||||
internal class LocalRepoCustomFormatJsonParser : IRadarrGuideService
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ISettingsProvider _settings;
|
||||
private readonly ILogger _log;
|
||||
private readonly string _repoPath;
|
||||
|
||||
public LocalRepoCustomFormatJsonParser(
|
||||
IFileSystem fileSystem,
|
||||
IResourcePaths paths,
|
||||
ISettingsProvider settings,
|
||||
ILogger log)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_settings = settings;
|
||||
_log = log;
|
||||
_repoPath = paths.RepoPath;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<string>> GetCustomFormatJsonAsync()
|
||||
{
|
||||
CloneOrUpdateGitRepo();
|
||||
namespace TrashLib.Radarr.CustomFormat.Guide;
|
||||
|
||||
var jsonDir = Path.Combine(_repoPath, "docs/json/radarr");
|
||||
var tasks = _fileSystem.Directory.GetFiles(jsonDir, "*.json")
|
||||
.Select(async f => await _fileSystem.File.ReadAllTextAsync(f));
|
||||
internal class LocalRepoCustomFormatJsonParser : IRadarrGuideService
|
||||
{
|
||||
private readonly ILogger _log;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IGitRepositoryFactory _repositoryFactory;
|
||||
private readonly IFileUtilities _fileUtils;
|
||||
private readonly ISettingsProvider _settingsProvider;
|
||||
private readonly string _repoPath;
|
||||
|
||||
return await Task.WhenAll(tasks);
|
||||
}
|
||||
public LocalRepoCustomFormatJsonParser(
|
||||
ILogger log,
|
||||
IFileSystem fileSystem,
|
||||
IResourcePaths paths,
|
||||
IGitRepositoryFactory repositoryFactory,
|
||||
IFileUtilities fileUtils,
|
||||
ISettingsProvider settingsProvider)
|
||||
{
|
||||
_log = log;
|
||||
_fileSystem = fileSystem;
|
||||
_repositoryFactory = repositoryFactory;
|
||||
_fileUtils = fileUtils;
|
||||
_settingsProvider = settingsProvider;
|
||||
_repoPath = paths.RepoPath;
|
||||
}
|
||||
|
||||
private void CloneOrUpdateGitRepo()
|
||||
public IEnumerable<string> GetCustomFormatJson()
|
||||
{
|
||||
// Retry only once if there's a failure. This gives us an opportunity to delete the git repository and start
|
||||
// fresh.
|
||||
var exception = CheckoutAndUpdateRepo();
|
||||
if (exception is not null)
|
||||
{
|
||||
var cloneUrl = _settings.Settings.Repository.CloneUrl;
|
||||
_log.Information("Deleting local git repo and retrying git operation...");
|
||||
_fileUtils.DeleteReadOnlyDirectory(_repoPath);
|
||||
|
||||
if (!Repository.IsValid(_repoPath))
|
||||
exception = CheckoutAndUpdateRepo();
|
||||
if (exception is not null)
|
||||
{
|
||||
if (_fileSystem.Directory.Exists(_repoPath))
|
||||
{
|
||||
_fileSystem.Directory.Delete(_repoPath, true);
|
||||
}
|
||||
|
||||
Repository.Clone(cloneUrl, _repoPath, new CloneOptions
|
||||
{
|
||||
RecurseSubmodules = false
|
||||
});
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
using var repo = new Repository(_repoPath);
|
||||
Commands.Checkout(repo, "master", new CheckoutOptions
|
||||
{
|
||||
CheckoutModifiers = CheckoutModifiers.Force
|
||||
});
|
||||
|
||||
var origin = repo.Network.Remotes["origin"];
|
||||
if (origin.Url != cloneUrl)
|
||||
{
|
||||
_log.Debug(
|
||||
"Origin's URL ({OriginUrl}) does not match the clone URL from settings ({CloneUrl}) and will be updated",
|
||||
origin.Url, cloneUrl);
|
||||
var jsonDir = Path.Combine(_repoPath, "docs/json/radarr");
|
||||
var tasks = _fileSystem.Directory.GetFiles(jsonDir, "*.json")
|
||||
.Select(async f => await _fileSystem.File.ReadAllTextAsync(f));
|
||||
|
||||
repo.Network.Remotes.Update("origin", updater => updater.Url = cloneUrl);
|
||||
origin = repo.Network.Remotes["origin"];
|
||||
}
|
||||
return Task.WhenAll(tasks).Result;
|
||||
}
|
||||
|
||||
Commands.Fetch(repo, origin.Name, origin.FetchRefSpecs.Select(s => s.Specification), null, "");
|
||||
private Exception? CheckoutAndUpdateRepo()
|
||||
{
|
||||
var repoSettings = _settingsProvider.Settings.Repository;
|
||||
var cloneUrl = repoSettings.CloneUrl;
|
||||
const string branch = "master";
|
||||
|
||||
repo.Reset(ResetMode.Hard, repo.Branches["origin/master"].Tip);
|
||||
try
|
||||
{
|
||||
using var repo = _repositoryFactory.CreateAndCloneIfNeeded(cloneUrl, _repoPath, branch);
|
||||
repo.ForceCheckout(branch);
|
||||
repo.Fetch();
|
||||
repo.ResetHard($"origin/{branch}");
|
||||
}
|
||||
catch (LibGit2SharpException e)
|
||||
{
|
||||
_log.Error(e, "An exception occurred during git operations on path: {RepoPath}", _repoPath);
|
||||
return e;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,50 @@
|
||||
using AutoFixture.NUnit3;
|
||||
using Common;
|
||||
using FluentAssertions;
|
||||
using LibGit2Sharp;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using TestLibrary.AutoFixture;
|
||||
using TestLibrary.NSubstitute;
|
||||
using VersionControl.Wrappers;
|
||||
|
||||
namespace VersionControl.Tests;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class GitRepositoryFactoryTest
|
||||
{
|
||||
[Test, AutoMockData]
|
||||
public void Delete_and_clone_when_repo_is_not_valid(
|
||||
[Frozen] IRepositoryStaticWrapper wrapper,
|
||||
[Frozen] IFileUtilities fileUtils,
|
||||
GitRepositoryFactory sut)
|
||||
{
|
||||
wrapper.IsValid(Arg.Any<string>()).Returns(false);
|
||||
|
||||
sut.CreateAndCloneIfNeeded("repo_url", "repo_path", "branch");
|
||||
|
||||
Received.InOrder(() =>
|
||||
{
|
||||
wrapper.IsValid("repo_path");
|
||||
fileUtils.DeleteReadOnlyDirectory("repo_path");
|
||||
wrapper.Clone("repo_url", "repo_path",
|
||||
Verify.That<CloneOptions>(x => x.BranchName.Should().Be("branch")));
|
||||
});
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void No_delete_and_clone_when_repo_is_valid(
|
||||
[Frozen] IRepositoryStaticWrapper wrapper,
|
||||
[Frozen] IFileUtilities fileUtils,
|
||||
GitRepositoryFactory sut)
|
||||
{
|
||||
wrapper.IsValid(Arg.Any<string>()).Returns(true);
|
||||
|
||||
sut.CreateAndCloneIfNeeded("repo_url", "repo_path", "branch");
|
||||
|
||||
wrapper.Received().IsValid("repo_path");
|
||||
fileUtils.DidNotReceiveWithAnyArgs().DeleteReadOnlyDirectory(default!);
|
||||
wrapper.DidNotReceiveWithAnyArgs().Clone(default!, default!, default!);
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
using System.Linq;
|
||||
using LibGit2Sharp;
|
||||
|
||||
namespace VersionControl;
|
||||
|
||||
public sealed class GitRepository : IGitRepository
|
||||
{
|
||||
private readonly Repository _repo;
|
||||
|
||||
public GitRepository(string repoPath)
|
||||
{
|
||||
_repo = new Repository(repoPath);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_repo.Dispose();
|
||||
}
|
||||
|
||||
public void ForceCheckout(string branch)
|
||||
{
|
||||
Commands.Checkout(_repo, branch, new CheckoutOptions
|
||||
{
|
||||
CheckoutModifiers = CheckoutModifiers.Force
|
||||
});
|
||||
}
|
||||
|
||||
public void Fetch(string remote = "origin")
|
||||
{
|
||||
var origin = _repo.Network.Remotes[remote];
|
||||
Commands.Fetch(_repo, origin.Name, origin.FetchRefSpecs.Select(s => s.Specification), null, "");
|
||||
}
|
||||
|
||||
public void ResetHard(string toBranch)
|
||||
{
|
||||
var commit = _repo.Branches[toBranch].Tip;
|
||||
_repo.Reset(ResetMode.Hard, commit);
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using Common;
|
||||
using LibGit2Sharp;
|
||||
using VersionControl.Wrappers;
|
||||
|
||||
namespace VersionControl;
|
||||
|
||||
public class GitRepositoryFactory : IGitRepositoryFactory
|
||||
{
|
||||
private readonly IFileUtilities _fileUtils;
|
||||
private readonly IRepositoryStaticWrapper _staticWrapper;
|
||||
private readonly Func<string, IGitRepository> _repoFactory;
|
||||
|
||||
public GitRepositoryFactory(
|
||||
IFileUtilities fileUtils,
|
||||
IRepositoryStaticWrapper staticWrapper,
|
||||
Func<string, IGitRepository> repoFactory)
|
||||
{
|
||||
_fileUtils = fileUtils;
|
||||
_staticWrapper = staticWrapper;
|
||||
_repoFactory = repoFactory;
|
||||
}
|
||||
|
||||
public IGitRepository CreateAndCloneIfNeeded(string repoUrl, string repoPath, string branch)
|
||||
{
|
||||
if (!_staticWrapper.IsValid(repoPath))
|
||||
{
|
||||
DeleteAndCloneRepo(repoUrl, repoPath, branch);
|
||||
}
|
||||
|
||||
return _repoFactory(repoPath);
|
||||
}
|
||||
|
||||
private void DeleteAndCloneRepo(string repoUrl, string repoPath, string branch)
|
||||
{
|
||||
_fileUtils.DeleteReadOnlyDirectory(repoPath);
|
||||
|
||||
Console.Write("Requesting and parsing guide markdown ");
|
||||
|
||||
using var progress = new ProgressBar();
|
||||
_staticWrapper.Clone(repoUrl, repoPath, new CloneOptions
|
||||
{
|
||||
RecurseSubmodules = false,
|
||||
BranchName = branch,
|
||||
OnTransferProgress = gitProgress =>
|
||||
{
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
progress.Report((float) gitProgress.ReceivedObjects / gitProgress.TotalObjects);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace VersionControl;
|
||||
|
||||
public interface IGitRepository : IDisposable
|
||||
{
|
||||
void ForceCheckout(string branch);
|
||||
void Fetch(string remote = "origin");
|
||||
void ResetHard(string toBranch);
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace VersionControl;
|
||||
|
||||
public interface IGitRepositoryFactory
|
||||
{
|
||||
IGitRepository CreateAndCloneIfNeeded(string repoUrl, string repoPath, string branch);
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
using Autofac;
|
||||
using VersionControl.Wrappers;
|
||||
|
||||
namespace VersionControl;
|
||||
|
||||
public class VersionControlAutofacModule : Module
|
||||
{
|
||||
protected override void Load(ContainerBuilder builder)
|
||||
{
|
||||
builder.RegisterType<GitRepository>().As<IGitRepository>();
|
||||
builder.RegisterType<LibGit2SharpRepositoryStaticWrapper>().As<IRepositoryStaticWrapper>();
|
||||
builder.RegisterType<GitRepositoryFactory>().As<IGitRepositoryFactory>();
|
||||
base.Load(builder);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using LibGit2Sharp;
|
||||
|
||||
namespace VersionControl.Wrappers;
|
||||
|
||||
public interface IRepositoryStaticWrapper
|
||||
{
|
||||
string Clone(string sourceUrl, string workdirPath, CloneOptions options);
|
||||
bool IsValid(string path);
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
using LibGit2Sharp;
|
||||
|
||||
namespace VersionControl.Wrappers;
|
||||
|
||||
public class LibGit2SharpRepositoryStaticWrapper : IRepositoryStaticWrapper
|
||||
{
|
||||
public string Clone(string sourceUrl, string workdirPath, CloneOptions options)
|
||||
=> Repository.Clone(sourceUrl, workdirPath, options);
|
||||
|
||||
public bool IsValid(string path)
|
||||
=> Repository.IsValid(path);
|
||||
}
|
Loading…
Reference in new issue