diff --git a/src/Tgstation.Server.Api/Models/Internal/RepositorySettings.cs b/src/Tgstation.Server.Api/Models/Internal/RepositorySettings.cs index c5ace7499c..14dac247ab 100644 --- a/src/Tgstation.Server.Api/Models/Internal/RepositorySettings.cs +++ b/src/Tgstation.Server.Api/Models/Internal/RepositorySettings.cs @@ -48,5 +48,19 @@ namespace Tgstation.Server.Api.Models.Internal [Permissions(WriteRight = RepositoryRights.ChangeTestMergeCommits)] [Required] public bool? ShowTestMergeCommitters { get; set; } + + /// + /// If test merge commits should be kept when auto updating. May cause merge conflicts which will block the update + /// + [Permissions(WriteRight = RepositoryRights.ChangeTestMergeCommits)] + [Required] + public bool? AutoUpdatesKeepTestMerges { get; set; } + + /// + /// If synchronization should occur when auto updating + /// + [Permissions(WriteRight = RepositoryRights.ChangeTestMergeCommits)] + [Required] + public bool? AutoUpdatesSynchronize { get; set; } } } diff --git a/src/Tgstation.Server.Api/Rights/RepositoryRights.cs b/src/Tgstation.Server.Api/Rights/RepositoryRights.cs index 23703fd446..faf745fa79 100644 --- a/src/Tgstation.Server.Api/Rights/RepositoryRights.cs +++ b/src/Tgstation.Server.Api/Rights/RepositoryRights.cs @@ -48,5 +48,9 @@ namespace Tgstation.Server.Api.Rights /// User may read all fields in the with the exception of /// Read = 512, + /// + /// User may change and + /// + ChangeAutoUpdateSettings = 1024 } } diff --git a/src/Tgstation.Server.Host/Components/EventType.cs b/src/Tgstation.Server.Host/Components/EventType.cs index 615c7ed3bb..e9442b0d10 100644 --- a/src/Tgstation.Server.Host/Components/EventType.cs +++ b/src/Tgstation.Server.Host/Components/EventType.cs @@ -10,7 +10,7 @@ /// RepoResetOrigin = 0, /// - /// Parameters: Reference name, commit sha + /// Parameters: Checkout target /// RepoCheckout = 1, /// @@ -18,18 +18,18 @@ /// RepoFetch = 2, /// - /// Parameters: Comma separated list in form of "#{Pull Request Number} @ {7 character SHA} + /// Parameters: Pull request number, pull request sha, merger name, merger message /// - RepoMergePullRequests = 3, + RepoMergePullRequest = 3, + /// + /// Parameters: Absolute path to repository root, committer name, committer email + /// + RepoPreSynchronize = 4, /// /// Parameters: Current version, new version /// - ByondChangeStart = 4, - /// - /// No parameters - /// - ByondChangeCancelled = 5, + ByondChangeStart = 5, /// /// Parameters: Error string /// @@ -37,36 +37,42 @@ /// /// No parameters /// - ByondStageComplete = 7, - /// - /// No parameters - /// - ByondChangeComplete = 8, + ByondChangeComplete = 7, /// /// Parameters: Origin commit sha /// - CompileStart = 9, + CompileStart = 8, /// /// No parameters /// - CompileCancelled = 10, + CompileCancelled = 9, /// /// Parameters: "1" if compile succeeded and api validation failed, "0" otherwise /// - CompileFailure = 11, + CompileFailure = 10, /// /// No parameters /// - CompileComplete = 12, + CompileComplete = 11, /// /// Parameters: Exit code /// - DDOtherCrash = 13, + DDOtherCrash = 12, /// /// No parameters /// - DDOtherExit = 14, + DDOtherExit = 13, + + /// + /// No parameters + /// + InstanceAutoUpdateStart = 14, + + /// + /// Parameters: Base sha, target sha, base reference, target reference + /// + RepoMergeConflict = 15, } } diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs index d134a34639..10412a6458 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs @@ -44,11 +44,11 @@ namespace Tgstation.Server.Host.Components.Repository /// The commit in the pull request to merge /// The name of the merge committer /// The e-mail of the merge committer - /// The body of the commit message /// The access string to fetch from the origin repository /// The for the operation + /// A string to identify the user that merged /// 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 accessString, CancellationToken cancellationToken); + Task AddTestMerge(int pullRequestNumber, string targetCommit, string committerName, string committerEmail, string accessString, string mergerIdentifier, CancellationToken cancellationToken); /// /// Fetch commits from the origin repository @@ -68,9 +68,11 @@ namespace Tgstation.Server.Host.Components.Repository /// /// Requires the current HEAD to be a tracked reference. Merges the reference to what it tracks on the origin repository /// + /// The name of the merge committer + /// The e-mail of the merge committer /// The for the operation /// A resulting in the SHA of the new HEAD. if the merge resulted in conflict - Task MergeOrigin(CancellationToken cancellationToken); + Task MergeOrigin(string committerName, string committerEmail, CancellationToken cancellationToken); /// /// Force push the current repository HEAD to ; diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index 52565c0a34..3e26156303 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -12,6 +12,8 @@ namespace Tgstation.Server.Host.Components.Repository /// sealed class Repository : IRepository { + const string UnknownReference = ""; + /// /// The branch name used for publishing testmerge commits /// @@ -39,6 +41,11 @@ namespace Tgstation.Server.Host.Components.Repository /// readonly IIOManager ioMananger; + /// + /// The for the + /// + readonly IEventConsumer eventConsumer; + /// /// to be taken when is called /// @@ -49,11 +56,13 @@ namespace Tgstation.Server.Host.Components.Repository /// /// The value of /// The value of + /// The value of /// The value if - public Repository(LibGit2Sharp.IRepository repository, IIOManager ioMananger, Action onDispose) + public Repository(LibGit2Sharp.IRepository repository, IIOManager ioMananger, IEventConsumer eventConsumer, Action onDispose) { this.repository = repository ?? throw new ArgumentNullException(nameof(repository)); this.ioMananger = ioMananger ?? throw new ArgumentNullException(nameof(ioMananger)); + this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); IsGitHubRepository = Origin.ToUpperInvariant().Contains("://GITHUB.COM/"); } @@ -97,79 +106,104 @@ namespace Tgstation.Server.Host.Components.Repository } /// - public Task AddTestMerge(int pullRequestNumber, string targetCommit, string committerName, string committerEmail, string commitBody, string accessString, CancellationToken cancellationToken) => Task.Factory.StartNew(() => + public async Task AddTestMerge(int pullRequestNumber, string targetCommit, string committerName, string committerEmail, string accessString, string mergerIdentifier, CancellationToken cancellationToken) { + + if (!IsGitHubRepository) + throw new InvalidOperationException("Test merging is only available on GitHub hosted origin repositories!"); + 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(Origin, 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 + MergeResult result = null; + await Task.Factory.StartNew(() => { - CommitOnSuccess = true, - FailOnConflict = true, - FastForwardStrategy = FastForwardStrategy.NoFastForward, - SkipReuc = true, - }); + var remote = repository.Network.Remotes.Add("temp_pr_fetch", GenerateAuthUrl(Origin, 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); + } - if (result.Status != MergeStatus.NonFastForward) + cancellationToken.ThrowIfCancellationRequested(); + + result = repository.Merge(targetCommit, new Signature(new Identity(committerName, committerEmail), DateTimeOffset.Now), new MergeOptions + { + CommitOnSuccess = true, + FailOnConflict = true, + FastForwardStrategy = FastForwardStrategy.NoFastForward, + SkipReuc = true + }); + + cancellationToken.ThrowIfCancellationRequested(); + + if (result.Status == MergeStatus.Conflicts) + { + RawCheckout(originalCommit.CanonicalName ?? originalCommit.Tip.Sha); + cancellationToken.ThrowIfCancellationRequested(); + } + + repository.RemoveUntrackedFiles(); + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); + + if (result.Status == MergeStatus.Conflicts) { - RawCheckout(originalCommit.FriendlyName ?? originalCommit.Tip.Sha); + await eventConsumer.HandleEvent(EventType.RepoMergeConflict, new List { originalCommit.Tip.Sha, targetCommit, originalCommit.FriendlyName ?? UnknownReference, prBranchName }, cancellationToken).ConfigureAwait(false); 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(() => + public async Task CheckoutObject(string committish, CancellationToken cancellationToken) { - var remote = repository.Network.Remotes.First(); - try + if (committish == null) + throw new ArgumentNullException(nameof(committish)); + await eventConsumer.HandleEvent(EventType.RepoCheckout, new List { committish }, cancellationToken).ConfigureAwait(false); + await Task.Factory.StartNew(() => RawCheckout(committish), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); + } + + /// + public Task FetchOrigin(string accessString, CancellationToken cancellationToken) => Task.WhenAll( + eventConsumer.HandleEvent(EventType.RepoFetch, Array.Empty(), cancellationToken), + Task.Factory.StartNew(() => { - Commands.Fetch((LibGit2Sharp.Repository)repository, remote.Name, remote.FetchRefSpecs.Select(x => x.Specification), new FetchOptions + var remote = repository.Network.Remotes.First(); + try { - 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); + 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(() => @@ -205,22 +239,26 @@ namespace Tgstation.Server.Host.Components.Repository { repository.Branches.Remove(branch); } - }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); /// - public Task ResetToOrigin(CancellationToken cancellationToken) => Task.Factory.StartNew(() => + public async Task ResetToOrigin(CancellationToken cancellationToken) { 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 + await eventConsumer.HandleEvent(EventType.RepoResetOrigin, new List { trackedBranch.FriendlyName, trackedBranch.Tip.Sha }, cancellationToken).ConfigureAwait(false); + return await Task.Factory.StartNew(() => { - CheckoutModifiers = CheckoutModifiers.Force - }); - repository.RemoveUntrackedFiles(); - return trackedBranch.Tip.Sha; - }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); + Commands.Checkout((LibGit2Sharp.Repository)repository, repository.Head.TrackedBranch, new CheckoutOptions + { + CheckoutModifiers = CheckoutModifiers.Force + }); + cancellationToken.ThrowIfCancellationRequested(); + repository.RemoveUntrackedFiles(); + return trackedBranch.Tip.Sha; + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); + } /// public async Task CopyTo(string path, CancellationToken cancellationToken) @@ -231,15 +269,84 @@ namespace Tgstation.Server.Host.Components.Repository } /// - public Task MergeOrigin(CancellationToken cancellationToken) + public async Task MergeOrigin(string committerName, string committerEmail, CancellationToken cancellationToken) { - throw new NotImplementedException(); + MergeResult result = null; + Branch trackedBranch = null; + + var oldHead = repository.Head; + + await Task.Factory.StartNew(() => + { + if (!repository.Head.IsTracking) + throw new InvalidOperationException("Cannot reset to origin while not on a tracked reference!"); + trackedBranch = repository.Head.TrackedBranch; + + result = repository.Merge(trackedBranch, new Signature(new Identity(committerName, committerEmail), DateTimeOffset.Now), new MergeOptions + { + CommitOnSuccess = true, + FailOnConflict = true, + FastForwardStrategy = FastForwardStrategy.Default, + SkipReuc = true, + }); + + cancellationToken.ThrowIfCancellationRequested(); + + if (result.Status == MergeStatus.Conflicts) + { + RawCheckout(oldHead.CanonicalName); + cancellationToken.ThrowIfCancellationRequested(); + } + + repository.RemoveUntrackedFiles(); + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); + + if (result.Status == MergeStatus.Conflicts) + { + await eventConsumer.HandleEvent(EventType.RepoMergeConflict, new List { oldHead.Tip.Sha, trackedBranch.Tip.Sha, oldHead.FriendlyName ?? UnknownReference, trackedBranch.FriendlyName }, cancellationToken).ConfigureAwait(false); + return null; + } + + return Head; } /// - public Task Sychronize(string accessString, CancellationToken cancellationToken) + public async Task Sychronize(string accessString, CancellationToken cancellationToken) { - throw new NotImplementedException(); + var startHead = Head; + + if (!await eventConsumer.HandleEvent(EventType.RepoPreSynchronize, new List { ioMananger.ResolvePath(".") }, cancellationToken).ConfigureAwait(false)) + return; + + if (Head == startHead || !repository.Head.IsTracking) + return; + + await Task.Factory.StartNew(() => + { + cancellationToken.ThrowIfCancellationRequested(); + var remote = repository.Network.Remotes.Add("temp_push", GenerateAuthUrl(Origin, accessString)); + try + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + repository.Network.Push(repository.Head, 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); + } + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); } } }