diff --git a/build/Version.props b/build/Version.props index 76db731fcd..a692f2d0ab 100644 --- a/build/Version.props +++ b/build/Version.props @@ -3,7 +3,7 @@ - 6.19.0 + 6.19.1 5.9.0 10.14.0 0.7.0 diff --git a/src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs index 1c526e9295..c110e98307 100644 --- a/src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs @@ -31,5 +31,9 @@ namespace Tgstation.Server.Host.Components.Repository TestMergeParameters parameters, RepositorySettings repositorySettings, CancellationToken cancellationToken) => throw new NotSupportedException(); + + /// + public ValueTask TransformRepositoryPassword(string? rawPassword, CancellationToken cancellationToken) + => ValueTask.FromResult(rawPassword); } } diff --git a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs index 97eded304c..4a71a65aea 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; + using Octokit; using Tgstation.Server.Api.Models; @@ -41,6 +42,19 @@ namespace Tgstation.Server.Host.Components.Repository this.gitHubServiceFactory = gitHubServiceFactory ?? throw new ArgumentNullException(nameof(gitHubServiceFactory)); } + /// + public override async ValueTask TransformRepositoryPassword(string? rawPassword, CancellationToken cancellationToken) + { + if (rawPassword == null) + return null; + + var gitHubService = await gitHubServiceFactory.CreateService( + rawPassword, + new RepositoryIdentifier(this), + cancellationToken); + return gitHubService?.GetGitPassword() ?? rawPassword; + } + /// protected override async ValueTask GetTestMergeImpl( TestMergeParameters parameters, diff --git a/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs index 58e2ebf274..05aa95946a 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs @@ -42,6 +42,10 @@ namespace Tgstation.Server.Host.Components.Repository { } + /// + public override ValueTask TransformRepositoryPassword(string? rawPassword, CancellationToken cancellationToken) + => ValueTask.FromResult(rawPassword); + /// protected override async ValueTask GetTestMergeImpl( TestMergeParameters parameters, diff --git a/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesBase.cs b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesBase.cs index 4aa58fd1fc..5ada23be9c 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesBase.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesBase.cs @@ -83,6 +83,9 @@ namespace Tgstation.Server.Host.Components.Repository return result; } + /// + public abstract ValueTask TransformRepositoryPassword(string? rawPassword, CancellationToken cancellationToken); + /// /// Implementation of . /// diff --git a/src/Tgstation.Server.Host/Components/Repository/ICredentialsProvider.cs b/src/Tgstation.Server.Host/Components/Repository/ICredentialsProvider.cs index 1324c35320..89c056b76b 100644 --- a/src/Tgstation.Server.Host/Components/Repository/ICredentialsProvider.cs +++ b/src/Tgstation.Server.Host/Components/Repository/ICredentialsProvider.cs @@ -1,4 +1,7 @@ -using LibGit2Sharp; +using System.Threading; +using System.Threading.Tasks; + +using LibGit2Sharp; using LibGit2Sharp.Handlers; using Tgstation.Server.Host.Jobs; @@ -13,10 +16,12 @@ namespace Tgstation.Server.Host.Components.Repository /// /// Generate a from a given and . /// + /// The for the repository in question. /// The optional username to use in the . /// The optional password to use in the . - /// A new . - CredentialsHandler GenerateCredentialsHandler(string? username, string? password); + /// The for the operation. + /// A resulting in a new . + ValueTask GenerateCredentialsHandler(IGitRemoteFeatures remoteFeatures, string? username, string? password, CancellationToken cancellationToken); /// /// Rethrow the authentication failure message as a if it is one. diff --git a/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeatures.cs index 4f48e8048d..2b21aded6c 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IGitRemoteFeatures.cs @@ -1,4 +1,7 @@ -namespace Tgstation.Server.Host.Components.Repository +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Components.Repository { /// /// Provides features for remote git services. @@ -14,5 +17,13 @@ /// Get. /// string TestMergeLocalBranchNameFormatter { get; } + + /// + /// Transform a service's into a password usable by git. + /// + /// The raw password to transform. + /// The for the operation. + /// A resulting in the transformed password. + public ValueTask TransformRepositoryPassword(string? rawPassword, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs b/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs index 418758ebe7..6d1bc093f9 100644 --- a/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs +++ b/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using LibGit2Sharp; using LibGit2Sharp.Handlers; + using Microsoft.Extensions.Logging; using Tgstation.Server.Api.Models; @@ -80,32 +81,38 @@ namespace Tgstation.Server.Host.Components.Repository TaskScheduler.Current); /// - public CredentialsHandler GenerateCredentialsHandler(string? username, string? password) => (a, b, supportedCredentialTypes) => + public async ValueTask GenerateCredentialsHandler(IGitRemoteFeatures gitRemoteFeatures, string? username, string? password, CancellationToken cancellationToken) { - var hasCreds = username != null; - var supportsUserPass = supportedCredentialTypes.HasFlag(SupportedCredentialTypes.UsernamePassword); - var supportsAnonymous = supportedCredentialTypes.HasFlag(SupportedCredentialTypes.Default); + ArgumentNullException.ThrowIfNull(gitRemoteFeatures); - logger.LogTrace( - "Credentials requested. Present: {credentialsPresent}. Supports anonymous: {credentialsSupportAnon}. Supports user/pass: {credentialsSupportUserPass}", - hasCreds, - supportsAnonymous, - supportsUserPass); - if (supportsUserPass && hasCreds) - return new UsernamePasswordCredentials - { - Username = username, - Password = password, - }; + var transformedPassword = await gitRemoteFeatures.TransformRepositoryPassword(password, cancellationToken); + return (a, b, supportedCredentialTypes) => + { + var hasCreds = username != null; + var supportsUserPass = supportedCredentialTypes.HasFlag(SupportedCredentialTypes.UsernamePassword); + var supportsAnonymous = supportedCredentialTypes.HasFlag(SupportedCredentialTypes.Default); - if (supportsAnonymous) - return new DefaultCredentials(); + logger.LogTrace( + "Credentials requested. Present: {credentialsPresent}. Supports anonymous: {credentialsSupportAnon}. Supports user/pass: {credentialsSupportUserPass}", + hasCreds, + supportsAnonymous, + supportsUserPass); + if (supportsUserPass && hasCreds) + return new UsernamePasswordCredentials + { + Username = username, + Password = transformedPassword, + }; - if (supportsUserPass) - throw new JobException(ErrorCode.RepoCredentialsRequired); + if (supportsAnonymous) + return new DefaultCredentials(); - throw new JobException(ErrorCode.RepoCannotAuthenticate); - }; + if (supportsUserPass) + throw new JobException(ErrorCode.RepoCredentialsRequired); + + throw new JobException(ErrorCode.RepoCannotAuthenticate); + }; + } /// public void CheckBadCredentialsException(LibGit2SharpException exception) diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index a62a50418b..84584d5823 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -126,6 +126,47 @@ namespace Tgstation.Server.Host.Components.Repository /// readonly ILogger logger; + /// + /// Generate a standard set of . + /// + /// of the operation. + /// The for the operation. + /// The for the operation. + /// A resulting in a new set of and the associated s based off . + static (PushOptions PushOptions, IEnumerable SubProgressReporters) GeneratePushOptions(JobProgressReporter progressReporter, CredentialsHandler credentialsHandler, CancellationToken cancellationToken) + { + var packFileCountingReporter = progressReporter.CreateSection(null, 0.25); + var packFileDeltafyingReporter = progressReporter.CreateSection(null, 0.25); + var transferProgressReporter = progressReporter.CreateSection(null, 0.5); + + return ( + PushOptions: new PushOptions + { + OnPackBuilderProgress = (stage, current, total) => + { + if (total < current) + total = current; + + var percentage = ((double)current) / total; + (stage == PackBuilderStage.Counting ? packFileCountingReporter : packFileDeltafyingReporter).ReportProgress(percentage); + return !cancellationToken.IsCancellationRequested; + }, + OnNegotiationCompletedBeforePush = (a) => !cancellationToken.IsCancellationRequested, + OnPushTransferProgress = (a, sentBytes, totalBytes) => + { + packFileCountingReporter.ReportProgress((double)sentBytes / totalBytes); + return !cancellationToken.IsCancellationRequested; + }, + CredentialsProvider = credentialsHandler, + }, + SubProgressReporters: new List + { + packFileCountingReporter, + packFileDeltafyingReporter, + transferProgressReporter, + }); + } + /// /// Initializes a new instance of the class. /// @@ -220,6 +261,8 @@ namespace Tgstation.Server.Host.Components.Repository var sig = new Signature(new Identity(committerName, committerEmail), DateTimeOffset.UtcNow); List? conflictedPaths = null; + + var credentialsHandler = await GenerateCredentialsHandler(username, password, cancellationToken); await Task.Factory.StartNew( () => { @@ -238,7 +281,7 @@ namespace Tgstation.Server.Host.Components.Repository new FetchOptions().Hydrate( logger, fetchReporter, - credentialsProvider.GenerateCredentialsHandler(username, password), + credentialsHandler, cancellationToken), logMessage); } @@ -425,14 +468,20 @@ namespace Tgstation.Server.Host.Components.Repository logger.LogDebug("Fetch origin..."); var parameters = new List(); + var credentialsHandlerTask = GenerateCredentialsHandler(username, password, cancellationToken); if (username != null) { parameters.Add(username); if (password != null) - parameters.Add(password); + { + var transformedPassword = await gitRemoteFeatures.TransformRepositoryPassword(password, cancellationToken); + if (transformedPassword != null) + parameters.Add(transformedPassword); + } } await eventConsumer.HandleEvent(EventType.RepoFetch, parameters, true, deploymentPipeline, cancellationToken); + var credentialsHandler = await credentialsHandlerTask; await Task.Factory.StartNew( () => { @@ -447,7 +496,7 @@ namespace Tgstation.Server.Host.Components.Repository }.Hydrate( logger, subReporter, - credentialsProvider.GenerateCredentialsHandler(username, password), + credentialsHandler, cancellationToken); commands.Fetch( @@ -725,9 +774,9 @@ namespace Tgstation.Server.Host.Components.Repository if (!synchronizeTrackedBranch) { using var progressReporter2 = progressReporter.CreateSection("Push to temporary branch", remainingProgressFactor); + var credentialsHandler = await GenerateCredentialsHandler(username, password, cancellationToken); await PushHeadToTemporaryBranch( - username, - password, + credentialsHandler, progressReporter2, cancellationToken); return false; @@ -741,6 +790,12 @@ namespace Tgstation.Server.Host.Components.Repository } logger.LogInformation("Synchronizing with origin..."); + using var pushReporter = progressReporter.CreateSection("Push to origin", remainingProgressFactor); + var (pushOptions, progressReporters) = await GeneratePushOptions( + pushReporter, + username, + password, + cancellationToken); return await Task.Factory.StartNew( () => @@ -748,12 +803,6 @@ namespace Tgstation.Server.Host.Components.Repository var remote = libGitRepo.Network.Remotes.First(); try { - using var pushReporter = progressReporter.CreateSection("Push to origin", remainingProgressFactor); - var (pushOptions, progressReporters) = GeneratePushOptions( - pushReporter, - username, - password, - cancellationToken); try { libGitRepo.Network.Push( @@ -993,12 +1042,11 @@ namespace Tgstation.Server.Host.Components.Repository /// /// Force push the current repository HEAD to ;. /// - /// The username to fetch from the origin repository. - /// The password to fetch from the origin repository. + /// The for the operation. /// of the operation. /// The for the operation. /// A representing the running operation. - Task PushHeadToTemporaryBranch(string username, string password, JobProgressReporter progressReporter, CancellationToken cancellationToken) => Task.Factory.StartNew( + Task PushHeadToTemporaryBranch(CredentialsHandler credentialsHandler, JobProgressReporter progressReporter, CancellationToken cancellationToken) => Task.Factory.StartNew( () => { logger.LogInformation("Pushing changes to temporary remote branch..."); @@ -1015,8 +1063,7 @@ namespace Tgstation.Server.Host.Components.Repository { var (pushOptions, progressReporters) = GeneratePushOptions( mainPushReporter, - username, - password, + credentialsHandler, cancellationToken); try @@ -1032,7 +1079,7 @@ namespace Tgstation.Server.Host.Components.Repository var removalString = String.Format(CultureInfo.InvariantCulture, ":{0}", branch.CanonicalName); using var forcePushReporter = progressReporter.CreateSection(null, 0.1); - var (forcePushOptions, forcePushReporters) = GeneratePushOptions(forcePushReporter, username, password, cancellationToken); + var (forcePushOptions, forcePushReporters) = GeneratePushOptions(forcePushReporter, credentialsHandler, cancellationToken); try { libGitRepo.Network.Push(remote, removalString, forcePushOptions); @@ -1068,40 +1115,12 @@ namespace Tgstation.Server.Host.Components.Repository /// The username for the . /// The password for the . /// The for the operation. - /// A new set of and the associated s based off . - (PushOptions PushOptions, IEnumerable SubProgressReporters) GeneratePushOptions(JobProgressReporter progressReporter, string username, string password, CancellationToken cancellationToken) - { - var packFileCountingReporter = progressReporter.CreateSection(null, 0.25); - var packFileDeltafyingReporter = progressReporter.CreateSection(null, 0.25); - var transferProgressReporter = progressReporter.CreateSection(null, 0.5); - - return ( - PushOptions: new PushOptions - { - OnPackBuilderProgress = (stage, current, total) => - { - if (total < current) - total = current; - - var percentage = ((double)current) / total; - (stage == PackBuilderStage.Counting ? packFileCountingReporter : packFileDeltafyingReporter).ReportProgress(percentage); - return !cancellationToken.IsCancellationRequested; - }, - OnNegotiationCompletedBeforePush = (a) => !cancellationToken.IsCancellationRequested, - OnPushTransferProgress = (a, sentBytes, totalBytes) => - { - packFileCountingReporter.ReportProgress((double)sentBytes / totalBytes); - return !cancellationToken.IsCancellationRequested; - }, - CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password), - }, - SubProgressReporters: new List - { - packFileCountingReporter, - packFileDeltafyingReporter, - transferProgressReporter, - }); - } + /// A resulting in a new set of and the associated s based off . + async ValueTask<(PushOptions PushOptions, IEnumerable SubProgressReporters)> GeneratePushOptions(JobProgressReporter progressReporter, string username, string password, CancellationToken cancellationToken) + => GeneratePushOptions( + progressReporter, + await GenerateCredentialsHandler(username, password, cancellationToken), + cancellationToken); /// /// Gets the path of . @@ -1153,10 +1172,12 @@ namespace Tgstation.Server.Host.Components.Repository using var fetchReporter = currentProgressReporter.CreateSection($"Fetch submodule {submodule.Name}", factor); + var credentialsHandler = await GenerateCredentialsHandler(username, password, cancellationToken); + submoduleUpdateOptions.FetchOptions.Hydrate( logger, fetchReporter, - credentialsProvider.GenerateCredentialsHandler(username, password), + credentialsHandler, cancellationToken); using var checkoutReporter = currentProgressReporter.CreateSection($"Checkout submodule {submodule.Name}", factor); @@ -1229,6 +1250,16 @@ namespace Tgstation.Server.Host.Components.Repository return RecursiveUpdateSubmodules(libGitRepo, progressReporter, GetRepositoryPath()); } + /// + /// Generate a from a given and . + /// + /// The optional username to use in the . + /// The optional password to use in the . + /// The for the operation. + /// A new . + ValueTask GenerateCredentialsHandler(string? username, string? password, CancellationToken cancellationToken) + => credentialsProvider.GenerateCredentialsHandler(gitRemoteFeatures, username, password, cancellationToken); + /// /// Converts a given to a . /// diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs index ddf05a7856..d06641da0d 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs @@ -166,7 +166,11 @@ namespace Tgstation.Server.Host.Components.Repository cloneOptions.FetchOptions.Hydrate( logger, cloneProgressReporter, - repositoryFactory.GenerateCredentialsHandler(username, password), + await repositoryFactory.GenerateCredentialsHandler( + gitRemoteFeaturesFactory.CreateGitRemoteFeatures(url), + username, + password, + cancellationToken), cancellationToken); await repositoryFactory.Clone( diff --git a/src/Tgstation.Server.Host/Utils/GitHub/GitHubService.cs b/src/Tgstation.Server.Host/Utils/GitHub/GitHubService.cs index 7a47fe2a9f..80df97ed1a 100644 --- a/src/Tgstation.Server.Host/Utils/GitHub/GitHubService.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/GitHubService.cs @@ -346,5 +346,9 @@ namespace Tgstation.Server.Host.Utils.GitHub committish) .WaitAsync(cancellationToken); } + + /// + public string GetGitPassword() + => gitHubClient.Connection.Credentials.Password; } } diff --git a/src/Tgstation.Server.Host/Utils/GitHub/IAuthenticatedGitHubService.cs b/src/Tgstation.Server.Host/Utils/GitHub/IAuthenticatedGitHubService.cs index b802e4731b..6d9b4e8706 100644 --- a/src/Tgstation.Server.Host/Utils/GitHub/IAuthenticatedGitHubService.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/IAuthenticatedGitHubService.cs @@ -10,6 +10,12 @@ namespace Tgstation.Server.Host.Utils.GitHub /// public interface IAuthenticatedGitHubService : IGitHubService { + /// + /// Gets the password to use for accessing a given repository. + /// + /// The password to use for accessing a GitHub repository with git. + string GetGitPassword(); + /// /// Create a comment on a given . /// diff --git a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs index b080816cb1..824220a429 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs @@ -1,10 +1,14 @@ -using LibGit2Sharp; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using LibGit2Sharp; + using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; + using Moq; -using System; -using System.IO; -using System.Threading.Tasks; namespace Tgstation.Server.Host.Components.Repository.Tests { @@ -14,7 +18,7 @@ namespace Tgstation.Server.Host.Components.Repository.Tests [TestClass] public sealed class TestRepositoryFactory { - static LibGit2RepositoryFactory CreateFactory() => new (Mock.Of>()); + static LibGit2RepositoryFactory CreateFactory() => new(Mock.Of>()); static async Task TestRepoLoading( string path, @@ -40,7 +44,7 @@ namespace Tgstation.Server.Host.Components.Repository.Tests { var factory = CreateFactory(); var cloneOpts = new CloneOptions(); - cloneOpts.FetchOptions.CredentialsProvider = factory.GenerateCredentialsHandler(null, null); + cloneOpts.FetchOptions.CredentialsProvider = await factory.GenerateCredentialsHandler(Mock.Of(), null, null, CancellationToken.None); await factory.Clone( new Uri("https://github.com/Cyberboss/Test"), cloneOpts, diff --git a/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs b/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs index d420bea426..61ba41c82f 100644 --- a/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs +++ b/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -23,6 +22,8 @@ namespace Tgstation.Server.Tests.Live { sealed class TestingGitHubService : IAuthenticatedGitHubService { + static string TestAccessToken => Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN"); + static Dictionary releasesDictionary; static PullRequest testPr; static GitHubCommit testCommit; @@ -37,7 +38,7 @@ namespace Tgstation.Server.Tests.Live var mockOptions = new Mock>(); mockOptions.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration { - GitHubAccessToken = Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN") + GitHubAccessToken = TestAccessToken, }); var gitHubClientFactory = new GitHubClientFactory(new AssemblyInformationProvider(), new BasicHttpMessageHandlerFactory(), mockOptions.Object, Mock.Of>()); @@ -162,5 +163,11 @@ namespace Tgstation.Server.Tests.Live logger.LogTrace("GetTgsReleases"); return ValueTask.FromResult(releasesDictionary); } + + public string GetGitPassword() + { + logger.LogTrace("GetGitPassword"); + return TestAccessToken; + } } } diff --git a/tests/Tgstation.Server.Tests/TestRepository.cs b/tests/Tgstation.Server.Tests/TestRepository.cs index e5c04e6cff..6aad5a5ab4 100644 --- a/tests/Tgstation.Server.Tests/TestRepository.cs +++ b/tests/Tgstation.Server.Tests/TestRepository.cs @@ -18,6 +18,7 @@ using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; +using Tgstation.Server.Host.Utils.GitHub; namespace Tgstation.Server.Tests { @@ -81,7 +82,10 @@ namespace Tgstation.Server.Tests tempPath), Mock.Of(), new WindowsPostWriteHandler(), - Mock.Of(), + new GitRemoteFeaturesFactory( + Mock.Of(), + Mock.Of(), + Mock.Of>()), Mock.Of>(), Mock.Of>(), Mock.Of>());