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
parent
f810749b32
commit
b34798eabb
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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 Autofac;
|
||||||
using VersionControl.Wrappers;
|
|
||||||
|
|
||||||
namespace VersionControl;
|
namespace TrashLib.Repo.VersionControl;
|
||||||
|
|
||||||
public class VersionControlAutofacModule : Module
|
public class VersionControlAutofacModule : Module
|
||||||
{
|
{
|
||||||
protected override void Load(ContainerBuilder builder)
|
protected override void Load(ContainerBuilder builder)
|
||||||
{
|
{
|
||||||
builder.RegisterType<GitRepository>().As<IGitRepository>();
|
builder.RegisterType<GitRepository>().As<IGitRepository>();
|
||||||
builder.RegisterType<LibGit2SharpRepositoryStaticWrapper>().As<IRepositoryStaticWrapper>();
|
|
||||||
builder.RegisterType<GitRepositoryFactory>().As<IGitRepositoryFactory>();
|
builder.RegisterType<GitRepositoryFactory>().As<IGitRepositoryFactory>();
|
||||||
|
builder.RegisterType<GitPath>().As<IGitPath>();
|
||||||
base.Load(builder);
|
base.Load(builder);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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…
Reference in new issue