From 722567ded8f4d1eebc514d0b219b49016c3e14d1 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 27 Jul 2020 22:23:58 -0400 Subject: [PATCH] Fix auto updates not clearing merged PRs --- .../Components/Instance.cs | 114 ++++++++++++++++-- .../Components/InstanceFactory.cs | 16 ++- .../Components/Repository/IRepository.cs | 11 +- .../Components/Repository/Repository.cs | 50 +++++++- .../Controllers/InstanceController.cs | 4 +- .../Tgstation.Server.Tests/IntegrationTest.cs | 27 +++++ 6 files changed, 205 insertions(+), 17 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index 83e4bca775..fbe5466555 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Octokit; using Serilog.Context; using System; using System.Collections.Generic; @@ -13,6 +14,9 @@ using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.Watchdog; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Models; @@ -60,6 +64,11 @@ namespace Tgstation.Server.Host.Components /// readonly IEventConsumer eventConsumer; + /// + /// The for the . + /// + readonly IGitHubClientFactory gitHubClientFactory; + /// /// The for the /// @@ -70,6 +79,11 @@ namespace Tgstation.Server.Host.Components /// readonly Api.Models.Instance metadata; + /// + /// The for the . + /// + readonly GeneralConfiguration generalConfiguration; + /// /// for and . /// @@ -98,7 +112,9 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of + /// The value of . /// The value of + /// The value of . public Instance( Api.Models.Instance metadata, IRepositoryManager repositoryManager, @@ -111,7 +127,9 @@ namespace Tgstation.Server.Host.Components IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, - ILogger logger) + IGitHubClientFactory gitHubClientFactory, + ILogger logger, + GeneralConfiguration generalConfiguration) { this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); @@ -123,7 +141,9 @@ namespace Tgstation.Server.Host.Components this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory)); this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); + this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration)); timerLock = new object(); } @@ -230,7 +250,7 @@ namespace Tgstation.Server.Host.Components .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge) .FirstOrDefaultAsync(jobCancellationToken); - async Task UpdateRevInfo(string currentHead, bool onOrigin) + async Task UpdateRevInfo(string currentHead, bool onOrigin, IEnumerable updatedTestMerges) { if (currentRevInfo == null) currentRevInfo = await LoadRevInfo().ConfigureAwait(false); @@ -253,7 +273,8 @@ namespace Tgstation.Server.Host.Components Instance = attachedInstance }; if (!onOrigin) - currentRevInfo.ActiveTestMerges = new List(oldRevInfo.ActiveTestMerges); + currentRevInfo.ActiveTestMerges = new List( + updatedTestMerges ?? oldRevInfo.ActiveTestMerges); databaseContext.Instances.Attach(attachedInstance); databaseContext.RevisionInformations.Add(currentRevInfo); @@ -261,8 +282,9 @@ namespace Tgstation.Server.Host.Components } // take appropriate auto update actions - bool shouldSyncTracked; - if (repositorySettings.AutoUpdatesKeepTestMerges.Value) + bool shouldSyncTracked = false; + bool preserveTestMerges = repositorySettings.AutoUpdatesKeepTestMerges.Value; + if (preserveTestMerges) { logger.LogTrace("Preserving test merges..."); @@ -275,21 +297,29 @@ namespace Tgstation.Server.Host.Components currentRevInfo = await currentRevInfoTask.ConfigureAwait(false); + var updatedTestMerges = await RemoveMergedPullRequests( + repo, + repositorySettings, + currentRevInfo, + cancellationToken) + .ConfigureAwait(false); + var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha; var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit; var currentHead = repo.Head; if (currentHead != startSha) { - await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false); + await UpdateRevInfo(currentHead, stillOnOrigin, updatedTestMerges).ConfigureAwait(false); shouldSyncTracked = stillOnOrigin; } else shouldSyncTracked = false; } - else + + if (!preserveTestMerges) { - logger.LogTrace("Not preserving test merges..."); + logger.LogTrace("Resetting to origin..."); await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false); var currentHead = repo.Head; @@ -301,7 +331,7 @@ namespace Tgstation.Server.Host.Components .ConfigureAwait(false); if (currentHead != startSha && currentRevInfo == default) - await UpdateRevInfo(currentHead, true).ConfigureAwait(false); + await UpdateRevInfo(currentHead, true, null).ConfigureAwait(false); shouldSyncTracked = true; } @@ -312,7 +342,7 @@ namespace Tgstation.Server.Host.Components var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false); var currentHead = repo.Head; if (currentHead != currentRevInfo.CommitSha) - await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false); + await UpdateRevInfo(currentHead, pushedOrigin, null).ConfigureAwait(false); } repoHead = repo.Head; @@ -393,7 +423,69 @@ namespace Tgstation.Server.Host.Components logger.LogTrace("Leaving auto update loop..."); } - #pragma warning restore CA1502 +#pragma warning restore CA1502 + + /// + /// Get the updated list of s for an origin merge. + /// + /// The to use. + /// The . + /// The current . + /// The for the operation. + /// A resulting in the of s that should remain the new . + async Task> RemoveMergedPullRequests( + IRepository repository, + RepositorySettings repositorySettings, + RevisionInformation revisionInformation, + CancellationToken cancellationToken) + { + if (revisionInformation.ActiveTestMerges?.Any() != true) + { + logger.LogTrace("No test merges to remove."); + return Array.Empty(); + } + + var gitHubClient = repositorySettings.AccessToken != null + ? gitHubClientFactory.CreateClient(repositorySettings.AccessToken) + : (String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken) + ? gitHubClientFactory.CreateClient() + : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken)); + + var tasks = revisionInformation + .ActiveTestMerges + .Select(x => gitHubClient + .PullRequest + .Get(repository.GitHubOwner, repository.GitHubRepoName, x.TestMerge.Number) + .WithToken(cancellationToken)); + try + { + await Task.WhenAll(tasks).ConfigureAwait(false); + } + catch (Exception ex) when (!(ex is OperationCanceledException)) + { + logger.LogWarning(ex, "Pull requests update check failed!"); + } + + var newList = revisionInformation.ActiveTestMerges.ToList(); + + async Task CheckRemovePR(Task task) + { + var pr = await task.ConfigureAwait(false); + if (!pr.Merged) + return; + + // We don't just assume, actually check the repo contains the merge commit. + if (await repository.ShaIsParent(pr.MergeCommitSha, cancellationToken).ConfigureAwait(false)) + newList.Remove( + newList.First( + potential => potential.TestMerge.Number == pr.Number)); + } + + foreach (var prTask in tasks) + await CheckRemovePR(prTask).ConfigureAwait(false); + + return newList; + } /// public Task InstanceRenamed(string newName, CancellationToken cancellationToken) diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 916b6950f4..dc6243b294 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using System; using System.Threading; using System.Threading.Tasks; @@ -11,6 +12,7 @@ using Tgstation.Server.Host.Components.Interop.Bridge; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Components.Watchdog; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.IO; @@ -123,6 +125,11 @@ namespace Tgstation.Server.Host.Components /// readonly IServerPortProvider serverPortProvider; + /// + /// The for the . + /// + readonly GeneralConfiguration generalConfiguration; + /// /// Construct an /// @@ -146,6 +153,7 @@ namespace Tgstation.Server.Host.Components /// The value of . /// The value of . /// The value of . + /// The containing the value of . public InstanceFactory( IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, @@ -166,7 +174,8 @@ namespace Tgstation.Server.Host.Components IPlatformIdentifier platformIdentifier, ILibGit2RepositoryFactory repositoryFactory, ILibGit2Commands repositoryCommands, - IServerPortProvider serverPortProvider) + IServerPortProvider serverPortProvider, + IOptions generalConfigurationOptions) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); @@ -188,6 +197,7 @@ namespace Tgstation.Server.Host.Components this.repositoryFactory = repositoryFactory ?? throw new ArgumentNullException(nameof(repositoryFactory)); this.repositoryCommands = repositoryCommands ?? throw new ArgumentNullException(nameof(repositoryCommands)); this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider)); + generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); } /// @@ -287,7 +297,9 @@ namespace Tgstation.Server.Host.Components dmbFactory, jobManager, eventConsumer, - loggerFactory.CreateLogger()); + gitHubClientFactory, + loggerFactory.CreateLogger(), + generalConfiguration); return instance; } diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs index 44cb5b39db..7790315cc2 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -132,5 +132,14 @@ namespace Tgstation.Server.Host.Components.Repository /// The for the operation /// A representing the running operation Task CopyTo(string path, CancellationToken cancellationToken); + + /// + /// Check if a given is a parent of the current . + /// + /// The SHA to check. + /// The for the operation. + /// A resulting in if is a parent of , otherwise. + /// This function is NOT reentrant. + Task ShaIsParent(string sha, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index 4db0962c02..1165e0f420 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -22,6 +22,16 @@ namespace Tgstation.Server.Host.Components.Repository /// public const string GitHubUrl = "://github.com/"; + /// + /// The default username for committers. + /// + public const string DefaultCommitterName = "tgstation-server"; + + /// + /// The default password for committers. + /// + public const string DefaultCommitterEmail = "tgstation-server@users.noreply.github.com"; + /// /// Template error message for when tracking of the most recent origin commit fails /// @@ -554,7 +564,7 @@ namespace Tgstation.Server.Host.Components.Repository trackedBranch = libGitRepo.Head.TrackedBranch; logger.LogDebug("Merge origin/{2}: <{0} ({1})>", committerName, committerEmail, trackedBranch.FriendlyName); - result = libGitRepo.Merge(trackedBranch, new Signature(new Identity(committerName, committerEmail), DateTimeOffset.Now), new MergeOptions + result = libGitRepo.Merge(trackedBranch, new Signature(committerName, committerEmail, DateTimeOffset.Now), new MergeOptions { CommitOnSuccess = true, FailOnConflict = true, @@ -708,5 +718,43 @@ namespace Tgstation.Server.Host.Components.Repository return true; return false; }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); + + /// + public Task ShaIsParent(string sha, CancellationToken cancellationToken) => Task.Factory.StartNew(() => + { + var targetCommit = libGitRepo.Lookup(sha); + if(targetCommit == null) + { + logger.LogTrace("Commit {0} not found in repository", sha); + return false; + } + + cancellationToken.ThrowIfCancellationRequested(); + var startSha = Head; + var mergeResult = libGitRepo.Merge( + targetCommit, + new Signature( + DefaultCommitterName, + DefaultCommitterEmail, + DateTimeOffset.Now), + new MergeOptions + { + FastForwardStrategy = FastForwardStrategy.FastForwardOnly, + FailOnConflict = true + }); + + if (mergeResult.Status == MergeStatus.UpToDate) + return true; + + commands.Checkout( + libGitRepo, + new CheckoutOptions + { + CheckoutModifiers = CheckoutModifiers.Force + }, + startSha); + + return false; + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); } } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 7483cecb69..243fe297db 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -126,8 +126,8 @@ namespace Tgstation.Server.Host.Controllers ChatBotLimit = initialSettings.ChatBotLimit ?? Models.Instance.DefaultChatBotLimit, RepositorySettings = new RepositorySettings { - CommitterEmail = "tgstation-server@users.noreply.github.com", - CommitterName = "tgstation-server", + CommitterEmail = Components.Repository.Repository.DefaultCommitterEmail, + CommitterName = Components.Repository.Repository.DefaultCommitterName, PushTestMergeCommits = false, ShowTestMergeCommitters = false, AutoUpdatesKeepTestMerges = false, diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index 127235d59d..3f96c59e2d 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -19,6 +19,8 @@ using System.Threading.Tasks; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; using Tgstation.Server.Client; +using Tgstation.Server.Host.Components.Events; +using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Database.Migrations; @@ -437,5 +439,30 @@ namespace Tgstation.Server.Tests Assert.AreEqual(String.Empty, (await process.GetErrorOutput(default)).Trim()); Assert.AreEqual("Hello World!", (await process.GetStandardOutput(default)).Trim()); } + + [TestMethod] + public async Task TestRepoParentLookup() + { + using var testingServer = new TestingServer(); + LibGit2Sharp.Repository.Clone("https://github.com/Cyberboss/test", testingServer.Directory); + var libGit2Repo = new LibGit2Sharp.Repository(testingServer.Directory); + using var repo = new Host.Components.Repository.Repository( + libGit2Repo, + new LibGit2Commands(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of>(), + () => { }); + + const string StartSha = "af4da8beb9f9b374b04a3cc4d65acca662e8cc1a"; + await repo.CheckoutObject(StartSha, progress => { }, default); + var result = await repo.ShaIsParent("2f8588a3ca0f6b027704a2a04381215619de3412", default); + Assert.IsTrue(result); + Assert.AreEqual(StartSha, repo.Head); + result = await repo.ShaIsParent("f636418bf47d238d33b0e4a34f0072b23a8aad0e", default); + Assert.IsFalse(result); ; + Assert.AreEqual(StartSha, repo.Head); + } } }