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.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace TrashLib.Radarr.CustomFormat.Guide;
|
namespace TrashLib.Radarr.CustomFormat.Guide;
|
||||||
|
|
||||||
public interface IRadarrGuideService
|
public interface IRadarrGuideService
|
||||||
{
|
{
|
||||||
Task<IEnumerable<string>> GetCustomFormatJsonAsync();
|
IEnumerable<string> GetCustomFormatJson();
|
||||||
}
|
}
|
||||||
|
@ -1,82 +1,86 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Abstractions;
|
using System.IO.Abstractions;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Common;
|
||||||
using LibGit2Sharp;
|
using LibGit2Sharp;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using TrashLib.Config.Settings;
|
using TrashLib.Config.Settings;
|
||||||
using TrashLib.Radarr.Config;
|
using TrashLib.Radarr.Config;
|
||||||
|
using VersionControl;
|
||||||
|
|
||||||
namespace TrashLib.Radarr.CustomFormat.Guide
|
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();
|
|
||||||
|
|
||||||
var jsonDir = Path.Combine(_repoPath, "docs/json/radarr");
|
internal class LocalRepoCustomFormatJsonParser : IRadarrGuideService
|
||||||
var tasks = _fileSystem.Directory.GetFiles(jsonDir, "*.json")
|
{
|
||||||
.Select(async f => await _fileSystem.File.ReadAllTextAsync(f));
|
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))
|
throw exception;
|
||||||
{
|
|
||||||
_fileSystem.Directory.Delete(_repoPath, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Repository.Clone(cloneUrl, _repoPath, new CloneOptions
|
|
||||||
{
|
|
||||||
RecurseSubmodules = false
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
using var repo = new Repository(_repoPath);
|
var jsonDir = Path.Combine(_repoPath, "docs/json/radarr");
|
||||||
Commands.Checkout(repo, "master", new CheckoutOptions
|
var tasks = _fileSystem.Directory.GetFiles(jsonDir, "*.json")
|
||||||
{
|
.Select(async f => await _fileSystem.File.ReadAllTextAsync(f));
|
||||||
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);
|
|
||||||
|
|
||||||
repo.Network.Remotes.Update("origin", updater => updater.Url = cloneUrl);
|
return Task.WhenAll(tasks).Result;
|
||||||
origin = repo.Network.Remotes["origin"];
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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