feat!: Remove LibGit2Sharp Library

This library was causing numerous issues related to git operations. The
straw that broke the camel's back is that it does not do automatic
garbage collection (`git gc --auto`). So a user's repo directory
continues to grow in size.

The replacement is CliWrap, which is just a simple wrapper library that
allows easy execution of shell commands. Specifically, `git` commands.

BREAKING CHANGE: This change now requires the `git` executable to be
installed by the user if run on a host system. The git executable will
be provided automatically for the docker image.
pull/151/head
Robert Dailey 1 year ago
parent f810749b32
commit b34798eabb

@ -20,9 +20,14 @@ changes you need to make.
`api_key` and `base_url` in a `secrets.yml` file. See [the secrets docs][secrets] for more info.
Huge thanks to @voltron4lyfe for this one. (#105, #139)
- Named instances are now supported in configuration YAML.
- New optional setting `repository.git_path` may be used to specify the path to a `git` executable.
If not used, `PATH` will be searched.
### Changed
- **BREAKING**: Recyclarr now requires `git` to be installed on host systems when using manual
installation. If using Docker, there is no breaking change since git will be bundled with the
image.
- Deprecated array-style instances in configuration YAML. Read more about this in the v3.0 Upgrade
Guide.

@ -2,7 +2,6 @@
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<!-- Central Package Versions -->
<ItemGroup>
<PackageVersion Include="Autofac" Version="6.4.0" />
@ -12,12 +11,12 @@
<PackageVersion Include="AutofacSerilogIntegration" Version="5.0.0" />
<PackageVersion Include="AutoMapper" Version="12.0.0" />
<PackageVersion Include="CliFx" Version="2.3.0" />
<PackageVersion Include="CliWrap" Version="3.5.0" />
<PackageVersion Include="FluentValidation" Version="11.2.2" />
<PackageVersion Include="Flurl" Version="3.0.6" />
<PackageVersion Include="Flurl.Http" Version="3.2.4" />
<PackageVersion Include="GitVersion.MsBuild" Version="5.11.1" />
<PackageVersion Include="JetBrains.Annotations" Version="2022.3.1" />
<PackageVersion Include="LibGit2Sharp" Version="0.27.0-preview-0182" />
<PackageVersion Include="morelinq" Version="3.3.2" />
<PackageVersion Include="MudBlazor" Version="6.0.18" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.1" />
@ -33,7 +32,6 @@
<PackageVersion Include="TestableIO.System.IO.Abstractions.Extensions" Version="1.0.32" />
<PackageVersion Include="YamlDotNet" Version="12.0.2" />
</ItemGroup>
<!-- Unit Test Packages -->
<ItemGroup>
<PackageVersion Include="AutofacContrib.NSubstitute" Version="7.0.0" />
@ -54,11 +52,10 @@
<PackageVersion Include="Serilog.Sinks.TestCorrelator" Version="3.2.0" />
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="17.2.3" />
</ItemGroup>
<!-- Following found during vulerabilities Code Scan -->
<ItemGroup>
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
</ItemGroup>
</Project>
</Project>

@ -17,7 +17,6 @@
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\TrashLib\TrashLib.csproj" />
<ProjectReference Include="..\VersionControl\VersionControl.csproj" />
</ItemGroup>
</Project>

@ -11,9 +11,8 @@ using Recyclarr.Command;
using Serilog;
using Serilog.Events;
using TrashLib;
using TrashLib.Repo.VersionControl;
using TrashLib.Startup;
using VersionControl;
using VersionControl.Wrappers;
namespace Recyclarr.TestLibrary;
@ -35,7 +34,6 @@ public abstract class IntegrationFixture : IDisposable
RegisterMockFor<IServiceCommand>(builder);
RegisterMockFor<IGitRepository>(builder);
RegisterMockFor<IGitRepositoryFactory>(builder);
RegisterMockFor<IRepositoryStaticWrapper>(builder);
RegisterExtraTypes(builder);

@ -13,8 +13,8 @@ using Recyclarr.Command;
using Serilog;
using TrashLib;
using TrashLib.Config.Services;
using TrashLib.Repo.VersionControl;
using TrashLib.Startup;
using VersionControl;
namespace Recyclarr.Tests;

@ -29,10 +29,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TrashLib", "TrashLib\TrashL
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TrashLib.Tests", "TrashLib.Tests\TrashLib.Tests.csproj", "{A4EC7E0D-C591-4874-B9AC-EB12A96F3E83}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VersionControl", "VersionControl\VersionControl.csproj", "{CF5BB1A7-3D21-48CB-B6B0-526612B2D94D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VersionControl.Tests", "VersionControl.Tests\VersionControl.Tests.csproj", "{F81C7EA3-4ACA-4171-8A60-531F129A33C5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Recyclarr.TestLibrary", "Recyclarr.TestLibrary\Recyclarr.TestLibrary.csproj", "{77D1C695-94D4-46A9-8F12-41E54AF97750}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Recyclarr.Gui", "Recyclarr.Gui\Recyclarr.Gui.csproj", "{53EECBC0-E0EA-4D6C-925C-5DB8C42CCB85}"
@ -81,14 +77,6 @@ Global
{A4EC7E0D-C591-4874-B9AC-EB12A96F3E83}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A4EC7E0D-C591-4874-B9AC-EB12A96F3E83}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A4EC7E0D-C591-4874-B9AC-EB12A96F3E83}.Release|Any CPU.Build.0 = Release|Any CPU
{CF5BB1A7-3D21-48CB-B6B0-526612B2D94D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CF5BB1A7-3D21-48CB-B6B0-526612B2D94D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CF5BB1A7-3D21-48CB-B6B0-526612B2D94D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CF5BB1A7-3D21-48CB-B6B0-526612B2D94D}.Release|Any CPU.Build.0 = Release|Any CPU
{F81C7EA3-4ACA-4171-8A60-531F129A33C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F81C7EA3-4ACA-4171-8A60-531F129A33C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F81C7EA3-4ACA-4171-8A60-531F129A33C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F81C7EA3-4ACA-4171-8A60-531F129A33C5}.Release|Any CPU.Build.0 = Release|Any CPU
{77D1C695-94D4-46A9-8F12-41E54AF97750}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{77D1C695-94D4-46A9-8F12-41E54AF97750}.Debug|Any CPU.Build.0 = Debug|Any CPU
{77D1C695-94D4-46A9-8F12-41E54AF97750}.Release|Any CPU.ActiveCfg = Release|Any CPU

@ -13,6 +13,7 @@ using Serilog;
using TrashLib.Config.Settings;
using TrashLib.Extensions;
using TrashLib.Repo;
using TrashLib.Repo.VersionControl;
using YamlDotNet.Core;
namespace Recyclarr.Command;
@ -62,13 +63,17 @@ public abstract class ServiceCommand : BaseCommand, IServiceCommand
throw new CommandException(
$"HTTP error while communicating with {Name}: {e.SanitizedExceptionMessage()}");
}
catch (GitCmdException e)
{
await console.Output.WriteLineAsync(e.ToString());
}
catch (Exception e) when (e is not CommandException)
{
throw new CommandException(e.ToString());
}
}
public override Task Process(ILifetimeScope container)
public override async Task Process(ILifetimeScope container)
{
var log = container.Resolve<ILogger>();
var settingsProvider = container.Resolve<ISettingsProvider>();
@ -81,9 +86,7 @@ public abstract class ServiceCommand : BaseCommand, IServiceCommand
migration.CheckNeededMigrations();
SetupHttp(log, settingsProvider);
repoUpdater.UpdateRepo();
return Task.CompletedTask;
await repoUpdater.UpdateRepo();
}
private static void SetupHttp(ILogger log, ISettingsProvider settingsProvider)

@ -14,13 +14,13 @@ using TrashLib.Cache;
using TrashLib.Config;
using TrashLib.Config.Services;
using TrashLib.Repo;
using TrashLib.Repo.VersionControl;
using TrashLib.Services.Common;
using TrashLib.Services.CustomFormat;
using TrashLib.Services.Radarr;
using TrashLib.Services.Sonarr;
using TrashLib.Services.System;
using TrashLib.Startup;
using VersionControl;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.ObjectFactories;

@ -0,0 +1,39 @@
using AutoFixture.NUnit3;
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using TestLibrary.AutoFixture;
using TrashLib.Config.Settings;
using TrashLib.Repo.VersionControl;
namespace TrashLib.Tests.Repo.VersionControl;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class GitPathTest
{
[Test, AutoMockData]
public void Default_path_used_when_setting_is_null(
[Frozen] ISettingsProvider settings,
GitPath sut)
{
settings.Settings.Returns(new SettingsValues {Repository = new TrashRepository {GitPath = null}});
var result = sut.Path;
result.Should().Be(GitPath.Default);
}
[Test, AutoMockData]
public void User_specified_path_used_instead_of_default(
[Frozen] ISettingsProvider settings,
GitPath sut)
{
var expectedPath = "/usr/local/bin/git";
settings.Settings.Returns(new SettingsValues {Repository = new TrashRepository {GitPath = expectedPath}});
var result = sut.Path;
result.Should().Be(expectedPath);
}
}

@ -7,6 +7,7 @@ public record TrashRepository
public string CloneUrl { get; [UsedImplicitly] init; } = "https://github.com/TRaSH-/Guides.git";
public string Branch { get; [UsedImplicitly] init; } = "master";
public string? Sha1 { get; [UsedImplicitly] init; }
public string? GitPath { get; [UsedImplicitly] init; }
}
public record LogJanitorSettings

@ -5,5 +5,5 @@ namespace TrashLib.Repo;
public interface IRepoUpdater
{
IDirectoryInfo RepoPath { get; }
void UpdateRepo();
Task UpdateRepo();
}

@ -1,10 +1,9 @@
using System.IO.Abstractions;
using Common;
using LibGit2Sharp;
using Serilog;
using TrashLib.Config.Settings;
using TrashLib.Repo.VersionControl;
using TrashLib.Startup;
using VersionControl;
namespace TrashLib.Repo;
@ -32,17 +31,17 @@ public class RepoUpdater : IRepoUpdater
public IDirectoryInfo RepoPath => _paths.RepoDirectory;
public void UpdateRepo()
public async Task UpdateRepo()
{
// Retry only once if there's a failure. This gives us an opportunity to delete the git repository and start
// fresh.
var exception = CheckoutAndUpdateRepo();
var exception = await CheckoutAndUpdateRepo();
if (exception is not null)
{
_log.Information("Deleting local git repo and retrying git operation...");
_fileUtils.DeleteReadOnlyDirectory(RepoPath.FullName);
exception = CheckoutAndUpdateRepo();
exception = await CheckoutAndUpdateRepo();
if (exception is not null)
{
throw exception;
@ -50,7 +49,7 @@ public class RepoUpdater : IRepoUpdater
}
}
private Exception? CheckoutAndUpdateRepo()
private async Task<Exception?> CheckoutAndUpdateRepo()
{
var repoSettings = _settingsProvider.Settings.Repository;
var cloneUrl = repoSettings.CloneUrl;
@ -64,12 +63,12 @@ public class RepoUpdater : IRepoUpdater
try
{
using var repo = _repositoryFactory.CreateAndCloneIfNeeded(cloneUrl, RepoPath.FullName, branch);
repo.ForceCheckout(branch);
repo.Fetch();
repo.ResetHard(repoSettings.Sha1 ?? $"origin/{branch}");
using var repo = await _repositoryFactory.CreateAndCloneIfNeeded(cloneUrl, RepoPath.FullName, branch);
await repo.ForceCheckout(branch);
await repo.Fetch();
await repo.ResetHard(repoSettings.Sha1 ?? $"origin/{branch}");
}
catch (LibGit2SharpException e)
catch (GitCmdException e)
{
_log.Error(e, "An exception occurred during git operations on path: {RepoPath}", RepoPath);
return e;

@ -0,0 +1,17 @@
namespace TrashLib.Repo.VersionControl;
public class GitCmdException : Exception
{
// ReSharper disable UnusedAutoPropertyAccessor.Global
public string Error { get; }
public int ExitCode { get; }
// ReSharper restore UnusedAutoPropertyAccessor.Global
public GitCmdException(int exitCode, string error)
: base("Git command failed with a non-zero exit code")
{
Error = error;
ExitCode = exitCode;
}
}

@ -0,0 +1,16 @@
using TrashLib.Config.Settings;
namespace TrashLib.Repo.VersionControl;
public class GitPath : IGitPath
{
private readonly ISettingsProvider _settings;
public GitPath(ISettingsProvider settings)
{
_settings = settings;
}
public static string Default => "git";
public string Path => _settings.Settings.Repository.GitPath ?? Default;
}

@ -0,0 +1,84 @@
using System.IO.Abstractions;
using System.Text;
using CliWrap;
using Serilog;
using TrashLib.Startup;
namespace TrashLib.Repo.VersionControl;
public sealed class GitRepository : IGitRepository
{
private readonly ILogger _log;
private readonly IAppPaths _paths;
private readonly IGitPath _gitPath;
public GitRepository(ILogger log, IAppPaths paths, IGitPath gitPath)
{
_log = log;
_paths = paths;
_gitPath = gitPath;
}
private async Task RunGitCmd(string args)
{
_log.Debug("Executing command: git {Args}", args);
var output = new StringBuilder();
var error = new StringBuilder();
var result = await Cli.Wrap(_gitPath.Path)
.WithArguments(args)
.WithValidation(CommandResultValidation.None)
.WithStandardOutputPipe(PipeTarget.ToStringBuilder(output))
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(error))
.WithWorkingDirectory(_paths.RepoDirectory.FullName)
.ExecuteAsync();
_log.Debug("{Output}", output.ToString());
if (result.ExitCode != 0)
{
throw new GitCmdException(result.ExitCode, error.ToString());
}
}
public IDirectoryInfo Path => _paths.RepoDirectory;
public void Dispose()
{
// Nothing to do here
}
public async Task ForceCheckout(string branch)
{
await RunGitCmd($"checkout -f {branch}");
}
public async Task Fetch(string remote = "origin")
{
await RunGitCmd($"fetch {remote}");
}
public async Task ResetHard(string toBranchOrSha1)
{
await RunGitCmd($"reset --hard {toBranchOrSha1}");
}
public async Task SetRemote(string name, string newUrl)
{
await RunGitCmd($"remote set-url {name} {newUrl}");
}
public async Task Clone(string cloneUrl, string? branch = null)
{
var args = new StringBuilder("clone");
if (branch is not null)
{
args.Append($" -b {branch}");
}
_paths.RepoDirectory.Create();
args.Append($" {cloneUrl} .");
await RunGitCmd(args.ToString());
}
}

@ -0,0 +1,52 @@
using System.IO.Abstractions;
using CliWrap;
using Common;
namespace TrashLib.Repo.VersionControl;
public class GitRepositoryFactory : IGitRepositoryFactory
{
private readonly IFileUtilities _fileUtils;
private readonly Func<string, IGitRepository> _repoFactory;
private readonly IGitPath _gitPath;
public GitRepositoryFactory(
IFileUtilities fileUtils,
Func<string, IGitRepository> repoFactory,
IGitPath gitPath)
{
_fileUtils = fileUtils;
_repoFactory = repoFactory;
_gitPath = gitPath;
}
private async Task<bool> IsValid(IDirectoryInfo repoPath)
{
var result = await Cli.Wrap(_gitPath.Path)
.WithArguments("status")
.WithValidation(CommandResultValidation.None)
.WithWorkingDirectory(repoPath.FullName)
.ExecuteAsync();
return result.ExitCode == 0;
}
public async Task<IGitRepository> CreateAndCloneIfNeeded(string repoUrl, string repoPath, string branch)
{
var repo = _repoFactory(repoPath);
if (!await IsValid(repo.Path))
{
await DeleteAndCloneRepo(repo, repoUrl, branch);
}
await repo.SetRemote("origin", repoUrl);
return repo;
}
private async Task DeleteAndCloneRepo(IGitRepository repo, string repoUrl, string branch)
{
_fileUtils.DeleteReadOnlyDirectory(repo.Path.FullName);
await repo.Clone(repoUrl, branch);
}
}

@ -0,0 +1,6 @@
namespace TrashLib.Repo.VersionControl;
public interface IGitPath
{
string Path { get; }
}

@ -0,0 +1,13 @@
using System.IO.Abstractions;
namespace TrashLib.Repo.VersionControl;
public interface IGitRepository : IDisposable
{
Task ForceCheckout(string branch);
Task Fetch(string remote = "origin");
Task ResetHard(string toBranchOrSha1);
Task SetRemote(string name, string newUrl);
IDirectoryInfo Path { get; }
Task Clone(string cloneUrl, string? branch = null);
}

@ -0,0 +1,6 @@
namespace TrashLib.Repo.VersionControl;
public interface IGitRepositoryFactory
{
Task<IGitRepository> CreateAndCloneIfNeeded(string repoUrl, string repoPath, string branch);
}

@ -1,15 +1,14 @@
using Autofac;
using VersionControl.Wrappers;
namespace VersionControl;
namespace TrashLib.Repo.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>();
builder.RegisterType<GitPath>().As<IGitPath>();
base.Load(builder);
}
}

@ -5,6 +5,7 @@
<PackageReference Include="Autofac.Extras.Ordering" />
<PackageReference Include="AutoMapper" />
<PackageReference Include="CliFx" />
<PackageReference Include="CliWrap" />
<PackageReference Include="FluentValidation" />
<PackageReference Include="Flurl" />
<PackageReference Include="Flurl.Http" />
@ -19,6 +20,5 @@
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\VersionControl\VersionControl.csproj" />
</ItemGroup>
</Project>

@ -1,59 +0,0 @@
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!);
}
[Test, AutoMockData]
public void Set_remote_when_creating_repository(
[Frozen] IGitRepository repo,
GitRepositoryFactory sut)
{
sut.CreateAndCloneIfNeeded("repo_url", "repo_path", "branch");
repo.Received().SetRemote("origin", "repo_url");
}
}

@ -1,9 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\TestLibrary\TestLibrary.csproj" />
<ProjectReference Include="..\VersionControl\VersionControl.csproj" />
</ItemGroup>
</Project>

@ -1,50 +0,0 @@
using Common.Extensions;
using LibGit2Sharp;
namespace VersionControl;
public sealed class GitRepository : IGitRepository
{
private readonly Lazy<Repository> _repo;
public GitRepository(string repoPath)
{
// Lazily construct the Repository object because it does too much work in its constructor
// We want to keep our own constructor here as thin as possible for DI and testability.
_repo = new Lazy<Repository>(() => new Repository(repoPath));
}
public void Dispose()
{
if (_repo.IsValueCreated)
{
_repo.Value.Dispose();
}
}
public void ForceCheckout(string branch)
{
Commands.Checkout(_repo.Value, branch, new CheckoutOptions
{
CheckoutModifiers = CheckoutModifiers.Force
});
}
public void Fetch(string remote = "origin")
{
var origin = _repo.Value.Network.Remotes[remote];
Commands.Fetch(_repo.Value, origin.Name, origin.FetchRefSpecs.Select(s => s.Specification), null, "");
}
public void ResetHard(string toBranchOrSha1)
{
var branch = _repo.Value.Branches.FirstOrDefault(b => b.FriendlyName.ContainsIgnoreCase(toBranchOrSha1));
var commit = branch is not null ? branch.Tip : _repo.Value.Lookup<Commit>(toBranchOrSha1);
_repo.Value.Reset(ResetMode.Hard, commit);
}
public void SetRemote(string name, string newUrl)
{
_repo.Value.Network.Remotes.Update(name, updater => updater.Url = newUrl);
}
}

@ -1,56 +0,0 @@
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;
private readonly Func<ProgressBar> _progressBarFactory;
public GitRepositoryFactory(
IFileUtilities fileUtils,
IRepositoryStaticWrapper staticWrapper,
Func<string, IGitRepository> repoFactory,
Func<ProgressBar> progressBarFactory)
{
_fileUtils = fileUtils;
_staticWrapper = staticWrapper;
_repoFactory = repoFactory;
_progressBarFactory = progressBarFactory;
}
public IGitRepository CreateAndCloneIfNeeded(string repoUrl, string repoPath, string branch)
{
if (!_staticWrapper.IsValid(repoPath))
{
DeleteAndCloneRepo(repoUrl, repoPath, branch);
}
var repo = _repoFactory(repoPath);
repo.SetRemote("origin", repoUrl);
return repo;
}
private void DeleteAndCloneRepo(string repoUrl, string repoPath, string branch)
{
_fileUtils.DeleteReadOnlyDirectory(repoPath);
var progress = _progressBarFactory();
progress.Description = "Fetching guide data\n";
_staticWrapper.Clone(repoUrl, repoPath, new CloneOptions
{
RecurseSubmodules = false,
BranchName = branch,
OnTransferProgress = gitProgress =>
{
progress.ReportProgress.OnNext((float) gitProgress.ReceivedObjects / gitProgress.TotalObjects);
return true;
}
});
}
}

@ -1,9 +0,0 @@
namespace VersionControl;
public interface IGitRepository : IDisposable
{
void ForceCheckout(string branch);
void Fetch(string remote = "origin");
void ResetHard(string toBranchOrSha1);
void SetRemote(string name, string newUrl);
}

@ -1,6 +0,0 @@
namespace VersionControl;
public interface IGitRepositoryFactory
{
IGitRepository CreateAndCloneIfNeeded(string repoUrl, string repoPath, string branch);
}

@ -1,8 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="LibGit2Sharp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

@ -1,9 +0,0 @@
using LibGit2Sharp;
namespace VersionControl.Wrappers;
public interface IRepositoryStaticWrapper
{
string Clone(string sourceUrl, string workdirPath, CloneOptions options);
bool IsValid(string path);
}

@ -1,12 +0,0 @@
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…
Cancel
Save