Implement repository

This commit is contained in:
Cyberboss
2018-04-17 12:16:24 -04:00
parent 7bf342844d
commit 982bed0674
3 changed files with 220 additions and 37 deletions
@@ -1,4 +1,5 @@
using System.Threading;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Components
@@ -6,51 +7,27 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// Represents an on-disk git repository
/// </summary>
interface IRepository
interface IRepository : IDisposable
{
/// <summary>
/// Check if the <see cref="IRepository"/> is in a working state
/// If the <see cref="IRepository"/> was cloned from GitHub.com
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the <see cref="IRepository"/> is in a working state, <see langword="false"/> otherwise</returns>
Task<bool> Exists(CancellationToken cancellationToken);
bool IsGitHubRepository { get; }
/// <summary>
/// Check if the <see cref="IRepository"/> was cloned from GitHub.com
/// The SHA of the <see cref="IRepository"/> HEAD
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the <see cref="IRepository"/> was cloned from GitHub.com, <see langword="false"/> otherwise</returns>
Task<bool> IsGitHubRepository(CancellationToken cancellationToken);
string Head { get; }
/// <summary>
/// Get the SHA of the <see cref="IRepository"/> HEAD
/// The current reference the <see cref="IRepository"/> HEAD is using. This can be a branch or tag
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the SHA of the <see cref="IRepository"/> HEAD</returns>
Task<string> GetHead(CancellationToken cancellationToken);
string Reference { get; }
/// <summary>
/// Get the current reference the <see cref="IRepository"/> HEAD is using. This can be a branch or tag
/// The current origin remote the <see cref="IRepository"/> is using
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the current reference the <see cref="IRepository"/> HEAD is using. Will be <see langword="null"/> if not on a branch or tag</returns>
Task<string> GetReference(CancellationToken cancellationToken);
/// <summary>
/// Get the current origin remote the <see cref="IRepository"/> is using
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the current origin remote the <see cref="IRepository"/> is using</returns>
Task<string> GetOrigin(CancellationToken cancellationToken);
/// <summary>
/// Deletes the <see cref="IRepository"/> and clones a <paramref name="newOrigin"/> using an <paramref name="accessString"/> if provided
/// </summary>
/// <param name="newOrigin">The new remote url</param>
/// <param name="accessString">The access string to clone the repository</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task SetOrigin(string newOrigin, string accessString, CancellationToken cancellationToken);
string Origin { get; }
/// <summary>
/// Checks out a given <paramref name="committish"/>
@@ -71,7 +48,7 @@ namespace Tgstation.Server.Host.Components
/// <param name="accessString">The access string to fetch from the origin repository</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the SHA of the new HEAD on success, <see langword="null"/> on merge conflict</returns>
Task<string> AddTestMerge(int pullRequestNumber, string targetCommit, string committerName, string committerEmail, string commitBody, string accessToken, CancellationToken cancellationToken);
Task<string> AddTestMerge(int pullRequestNumber, string targetCommit, string committerName, string committerEmail, string commitBody, string accessString, CancellationToken cancellationToken);
/// <summary>
/// Fetch commits from the origin repository
@@ -89,10 +66,11 @@ namespace Tgstation.Server.Host.Components
Task<string> ResetToOrigin(CancellationToken cancellationToken);
/// <summary>
/// Push the current repository HEAD to a temporary GitHub branch and then delete it
/// Force push the current repository HEAD to <see cref="Repository.RemoteTemporaryBranchName"/>;
/// </summary>
/// <param name="accessString">The access string to fetch from the origin repository</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task PushHeadToTemporaryBranch(CancellationToken cancellationToken);
Task PushHeadToTemporaryBranch(string accessString, CancellationToken cancellationToken);
}
}
@@ -0,0 +1,204 @@
using LibGit2Sharp;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Components
{
/// <inheritdoc />
sealed class Repository : IRepository
{
/// <summary>
/// The branch name used for publishing testmerge commits
/// </summary>
public const string RemoteTemporaryBranchName = "___TGSTempBranch";
/// <inheritdoc />
public bool IsGitHubRepository { get; }
/// <inheritdoc />
public string Head => repository.Head.Tip.Sha;
/// <inheritdoc />
public string Reference => repository.Head.FriendlyName;
/// <inheritdoc />
public string Origin => repository.Network.Remotes.First().Url;
/// <summary>
/// The <see cref="LibGit2Sharp.IRepository"/> for the <see cref="Repository"/>
/// </summary>
readonly LibGit2Sharp.IRepository repository;
/// <summary>
/// Construct a <see cref="Repository"/>
/// </summary>
/// <param name="repository">The value of <see cref="repository"/></param>
public Repository(LibGit2Sharp.IRepository repository)
{
this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
IsGitHubRepository = Origin.ToUpperInvariant().Contains("GITHUB.COM");
}
/// <inheritdoc />
public void Dispose() => repository.Dispose();
/// <summary>
/// Convert <see cref="Origin"/> to an "https://<paramref name="accessString"/>@{url} equivalent
/// </summary>
/// <param name="accessString">The <see cref="string"/> containing authentication info for the remote repository</param>
/// <returns>An authenticated URL for accessing the remote repository</returns>
string GenerateAuthUrl(string accessString)
{
if (String.IsNullOrWhiteSpace(accessString))
return Origin;
const string HttProtocolSecure = "HTTPS://";
if (!Origin.ToUpperInvariant().StartsWith(HttProtocolSecure, StringComparison.InvariantCulture))
throw new InvalidOperationException("Cannot use access string without HTTPS remote!");
//ONLY support https urls
return Origin.ToUpperInvariant().Replace(HttProtocolSecure, String.Concat(HttProtocolSecure, accessString, '@'));
}
/// <summary>
/// Runs a blocking force checkout to <paramref name="committish"/>
/// </summary>
/// <param name="committish">The committish to checkout</param>
void RawCheckout(string committish)
{
Commands.Checkout(repository, committish, new CheckoutOptions
{
CheckoutModifiers = CheckoutModifiers.Force
});
repository.RemoveUntrackedFiles();
}
/// <inheritdoc />
public Task<string> AddTestMerge(int pullRequestNumber, string targetCommit, string committerName, string committerEmail, string commitBody, string accessString, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
{
var Refspec = new List<string>();
var prBranchName = String.Format(CultureInfo.InvariantCulture, "pr-{0}", pullRequestNumber);
var localBranchName = String.Format(CultureInfo.InvariantCulture, "pull/{0}/headrefs/heads/{1}", pullRequestNumber, prBranchName);
Refspec.Add(String.Format(CultureInfo.InvariantCulture, "pull/{0}/head:{1}", pullRequestNumber, prBranchName));
var logMessage = String.Format(CultureInfo.InvariantCulture, "Merge remote pull request #{0}", pullRequestNumber);
var remote = repository.Network.Remotes.Add("temp_pr_fetch", GenerateAuthUrl(accessString));
try
{
cancellationToken.ThrowIfCancellationRequested();
Commands.Fetch((LibGit2Sharp.Repository)repository, remote.Name, Refspec, new FetchOptions
{
Prune = true,
OnProgress = (a) => !cancellationToken.IsCancellationRequested,
OnTransferProgress = (a) => !cancellationToken.IsCancellationRequested,
OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested
}, logMessage);
}
catch (UserCancelledException) { }
finally
{
repository.Network.Remotes.Remove(remote.Name);
//commit is there and we never gc so
repository.Branches.Remove(localBranchName);
repository.Branches.Remove(prBranchName);
}
cancellationToken.ThrowIfCancellationRequested();
var originalCommit = repository.Head;
var result = repository.Merge(targetCommit, new Signature(new Identity(committerName, committerEmail), DateTimeOffset.Now), new MergeOptions
{
CommitOnSuccess = true,
FailOnConflict = true,
FastForwardStrategy = FastForwardStrategy.NoFastForward,
SkipReuc = true,
});
if (result.Status != MergeStatus.NonFastForward)
{
RawCheckout(originalCommit.FriendlyName ?? originalCommit.Tip.Sha);
return null;
}
return result.Commit.Sha;
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
/// <inheritdoc />
public Task CheckoutObject(string committish, CancellationToken cancellationToken) => Task.Factory.StartNew(() => RawCheckout(committish), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
/// <inheritdoc />
public Task FetchOrigin(string accessString, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
{
var remote = repository.Network.Remotes.First();
try
{
Commands.Fetch((LibGit2Sharp.Repository)repository, remote.Name, remote.FetchRefSpecs.Select(x => x.Specification), new FetchOptions
{
Prune = true,
OnProgress = (a) => !cancellationToken.IsCancellationRequested,
OnTransferProgress = (a) => !cancellationToken.IsCancellationRequested,
OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested
}, "Fetch origin commits");
}
catch (UserCancelledException)
{
cancellationToken.ThrowIfCancellationRequested();
}
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
/// <inheritdoc />
public Task PushHeadToTemporaryBranch(string accessString, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
{
var branch = repository.CreateBranch(RemoteTemporaryBranchName);
try
{
cancellationToken.ThrowIfCancellationRequested();
var remote = repository.Network.Remotes.Add("temp_push", GenerateAuthUrl(accessString));
try
{
cancellationToken.ThrowIfCancellationRequested();
try
{
repository.Network.Push(remote, String.Format(CultureInfo.InvariantCulture, "+{0}:{0}", branch.CanonicalName), new PushOptions
{
OnPackBuilderProgress = (a, b, c) => !cancellationToken.IsCancellationRequested,
OnNegotiationCompletedBeforePush = (a) => !cancellationToken.IsCancellationRequested,
OnPushTransferProgress = (a, b, c) => !cancellationToken.IsCancellationRequested
});
}
catch (UserCancelledException)
{
cancellationToken.ThrowIfCancellationRequested();
}
}
finally
{
repository.Network.Remotes.Remove(remote.Name);
}
}
finally
{
repository.Branches.Remove(branch);
}
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
/// <inheritdoc />
public Task<string> ResetToOrigin(CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
{
if (!repository.Head.IsTracking)
throw new InvalidOperationException("Cannot reset to origin while not on a tracked reference!");
var trackedBranch = repository.Head.TrackedBranch;
Commands.Checkout((LibGit2Sharp.Repository)repository, repository.Head.TrackedBranch, new CheckoutOptions
{
CheckoutModifiers = CheckoutModifiers.Force
});
repository.RemoveUntrackedFiles();
return trackedBranch.Tip.Sha;
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
}
}
@@ -35,6 +35,7 @@
<ItemGroup>
<PackageReference Include="Byond.TopicSender" Version="1.1.0.1" />
<PackageReference Include="Cyberboss.AspNetCore.AsyncInitializer" Version="1.1.0" />
<PackageReference Include="LibGit2Sharp" Version="0.26.0-preview-0017" />
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.0-preview2-final" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.1.0-preview2-final" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.1.0-preview2-final" />