diff --git a/src/Tgstation.Server.Host/Components/IRepository.cs b/src/Tgstation.Server.Host/Components/IRepository.cs
index 90dce08440..a1c1f2c663 100644
--- a/src/Tgstation.Server.Host/Components/IRepository.cs
+++ b/src/Tgstation.Server.Host/Components/IRepository.cs
@@ -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
///
/// Represents an on-disk git repository
///
- interface IRepository
+ interface IRepository : IDisposable
{
///
- /// Check if the is in a working state
+ /// If the was cloned from GitHub.com
///
- /// The for the operation
- /// A resulting in if the is in a working state, otherwise
- Task Exists(CancellationToken cancellationToken);
+ bool IsGitHubRepository { get; }
///
- /// Check if the was cloned from GitHub.com
+ /// The SHA of the HEAD
///
- /// The for the operation
- /// A resulting in if the was cloned from GitHub.com, otherwise
- Task IsGitHubRepository(CancellationToken cancellationToken);
+ string Head { get; }
///
- /// Get the SHA of the HEAD
+ /// The current reference the HEAD is using. This can be a branch or tag
///
- /// The for the operation
- /// A resulting in the SHA of the HEAD
- Task GetHead(CancellationToken cancellationToken);
+ string Reference { get; }
///
- /// Get the current reference the HEAD is using. This can be a branch or tag
+ /// The current origin remote the is using
///
- /// The for the operation
- /// A resulting in the current reference the HEAD is using. Will be if not on a branch or tag
- Task GetReference(CancellationToken cancellationToken);
-
- ///
- /// Get the current origin remote the is using
- ///
- /// The for the operation
- /// A resulting in the current origin remote the is using
- Task GetOrigin(CancellationToken cancellationToken);
-
- ///
- /// Deletes the and clones a using an if provided
- ///
- /// The new remote url
- /// The access string to clone the repository
- /// The for the operation
- /// A representing the running operation
- Task SetOrigin(string newOrigin, string accessString, CancellationToken cancellationToken);
+ string Origin { get; }
///
/// Checks out a given
@@ -71,7 +48,7 @@ namespace Tgstation.Server.Host.Components
/// The access string to fetch from the origin repository
/// The for the operation
/// A resulting in the SHA of the new HEAD on success, on merge conflict
- Task AddTestMerge(int pullRequestNumber, string targetCommit, string committerName, string committerEmail, string commitBody, string accessToken, CancellationToken cancellationToken);
+ Task AddTestMerge(int pullRequestNumber, string targetCommit, string committerName, string committerEmail, string commitBody, string accessString, CancellationToken cancellationToken);
///
/// Fetch commits from the origin repository
@@ -89,10 +66,11 @@ namespace Tgstation.Server.Host.Components
Task ResetToOrigin(CancellationToken cancellationToken);
///
- /// Push the current repository HEAD to a temporary GitHub branch and then delete it
+ /// Force push the current repository HEAD to ;
///
+ /// The access string to fetch from the origin repository
/// The for the operation
/// A representing the running operation
- Task PushHeadToTemporaryBranch(CancellationToken cancellationToken);
+ Task PushHeadToTemporaryBranch(string accessString, CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Host/Components/Repository.cs b/src/Tgstation.Server.Host/Components/Repository.cs
new file mode 100644
index 0000000000..aa031d6fee
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Repository.cs
@@ -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
+{
+ ///
+ sealed class Repository : IRepository
+ {
+ ///
+ /// The branch name used for publishing testmerge commits
+ ///
+ public const string RemoteTemporaryBranchName = "___TGSTempBranch";
+
+ ///
+ public bool IsGitHubRepository { get; }
+
+ ///
+ public string Head => repository.Head.Tip.Sha;
+
+ ///
+ public string Reference => repository.Head.FriendlyName;
+
+ ///
+ public string Origin => repository.Network.Remotes.First().Url;
+
+ ///
+ /// The for the
+ ///
+ readonly LibGit2Sharp.IRepository repository;
+
+ ///
+ /// Construct a
+ ///
+ /// The value of
+ public Repository(LibGit2Sharp.IRepository repository)
+ {
+ this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
+ IsGitHubRepository = Origin.ToUpperInvariant().Contains("GITHUB.COM");
+ }
+
+ ///
+ public void Dispose() => repository.Dispose();
+
+ ///
+ /// Convert to an "https://@{url} equivalent
+ ///
+ /// The containing authentication info for the remote repository
+ /// An authenticated URL for accessing the remote repository
+ 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, '@'));
+ }
+
+ ///
+ /// Runs a blocking force checkout to
+ ///
+ /// The committish to checkout
+ void RawCheckout(string committish)
+ {
+ Commands.Checkout(repository, committish, new CheckoutOptions
+ {
+ CheckoutModifiers = CheckoutModifiers.Force
+ });
+ repository.RemoveUntrackedFiles();
+ }
+
+ ///
+ public Task AddTestMerge(int pullRequestNumber, string targetCommit, string committerName, string committerEmail, string commitBody, string accessString, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
+ {
+ var Refspec = new List();
+ 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);
+
+ ///
+ public Task CheckoutObject(string committish, CancellationToken cancellationToken) => Task.Factory.StartNew(() => RawCheckout(committish), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
+
+ ///
+ 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);
+
+ ///
+ 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);
+
+ ///
+ public Task 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);
+ }
+}
diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
index 2370aabf52..2dfa73f107 100644
--- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
+++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
@@ -35,6 +35,7 @@
+