From 982bed0674b79a83d37b2550077bb36224f6afee Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Tue, 17 Apr 2018 12:16:24 -0400 Subject: [PATCH] Implement repository --- .../Components/IRepository.cs | 52 ++--- .../Components/Repository.cs | 204 ++++++++++++++++++ .../Tgstation.Server.Host.csproj | 1 + 3 files changed, 220 insertions(+), 37 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/Repository.cs 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 @@ +