mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-30 16:39:21 +01:00
Implement repository
This commit is contained in:
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user