mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-27 07:04:57 +01:00
Merge pull request #1407 from tgstation/dev [APIDeploy][TGSDeploy]
v5.2.0
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<!-- This is in it's own file to help incremental building, changing it causes a complete rebuild of the web panel -->
|
||||
<TgsControlPanelVersion>4.0.0</TgsControlPanelVersion>
|
||||
<TgsControlPanelVersion>4.2.2</TgsControlPanelVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
+4
-4
@@ -3,11 +3,11 @@
|
||||
<!-- Integration tests will ensure they match across the board -->
|
||||
<Import Project="ControlPanelVersion.props" />
|
||||
<PropertyGroup>
|
||||
<TgsCoreVersion>5.1.4</TgsCoreVersion>
|
||||
<TgsCoreVersion>5.2.0</TgsCoreVersion>
|
||||
<TgsConfigVersion>4.4.0</TgsConfigVersion>
|
||||
<TgsApiVersion>9.6.0</TgsApiVersion>
|
||||
<TgsApiLibraryVersion>10.0.0</TgsApiLibraryVersion>
|
||||
<TgsClientVersion>11.0.0</TgsClientVersion>
|
||||
<TgsApiVersion>9.7.0</TgsApiVersion>
|
||||
<TgsApiLibraryVersion>10.1.0</TgsApiLibraryVersion>
|
||||
<TgsClientVersion>11.1.0</TgsClientVersion>
|
||||
<TgsDmapiVersion>6.0.5</TgsDmapiVersion>
|
||||
<TgsInteropVersion>5.3.0</TgsInteropVersion>
|
||||
<TgsHostWatchdogVersion>1.2.0</TgsHostWatchdogVersion>
|
||||
|
||||
@@ -24,6 +24,11 @@ namespace Tgstation.Server.Api.Models
|
||||
/// </summary>
|
||||
public bool BasedMeme { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the tgstation-server logo is shown in deployment embeds.
|
||||
/// </summary>
|
||||
public bool DeploymentBranding { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DiscordDMOutputDisplayType"/>.
|
||||
/// </summary>
|
||||
@@ -47,21 +52,25 @@ namespace Tgstation.Server.Api.Models
|
||||
throw new ArgumentNullException(nameof(connectionString));
|
||||
|
||||
var splits = connectionString.Split(';');
|
||||
|
||||
BotToken = splits.First();
|
||||
|
||||
if (splits.Length < 2 || !Enum.TryParse<DiscordDMOutputDisplayType>(splits[1], out var dMOutputDisplayType))
|
||||
dMOutputDisplayType = DiscordDMOutputDisplayType.Always;
|
||||
DMOutputDisplay = dMOutputDisplayType;
|
||||
|
||||
if (splits.Length > 2 && Int32.TryParse(splits[2], out Int32 basedMeme))
|
||||
{
|
||||
BasedMeme = Convert.ToBoolean(basedMeme);
|
||||
}
|
||||
else
|
||||
{
|
||||
BasedMeme = true; // oranges said this needs to be true by default :pensive:
|
||||
}
|
||||
|
||||
if (splits.Length > 3 && Int32.TryParse(splits[3], out Int32 branding))
|
||||
DeploymentBranding = Convert.ToBoolean(branding);
|
||||
else
|
||||
DeploymentBranding = true; // previous default behaviour
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => $"{BotToken};{(int)DMOutputDisplay};{Convert.ToInt32(BasedMeme)}";
|
||||
public override string ToString() => $"{BotToken};{(int)DMOutputDisplay};{Convert.ToInt32(BasedMeme)};{Convert.ToInt32(DeploymentBranding)}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// </summary>
|
||||
readonly bool basedMeme;
|
||||
|
||||
/// <summary>
|
||||
/// If the tgstation-server logo is shown in deployment embeds.
|
||||
/// </summary>
|
||||
readonly bool deploymentBranding;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DiscordDMOutputDisplayType"/>.
|
||||
/// </summary>
|
||||
@@ -177,6 +182,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
var botToken = csb.BotToken;
|
||||
basedMeme = csb.BasedMeme;
|
||||
outputDisplayType = csb.DMOutputDisplay;
|
||||
deploymentBranding = csb.DeploymentBranding;
|
||||
|
||||
serviceProvider = new ServiceCollection()
|
||||
.AddDiscordGateway(serviceProvider => botToken)
|
||||
@@ -282,13 +288,14 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
localCommitPushed |= revisionInformation.CommitSha == revisionInformation.OriginCommitSha;
|
||||
|
||||
var fields = BuildUpdateEmbedFields(revisionInformation, byondVersion, gitHubOwner, gitHubRepo, localCommitPushed);
|
||||
var author = new EmbedAuthor(assemblyInformationProvider.VersionPrefix)
|
||||
{
|
||||
Url = "https://github.com/tgstation/tgstation-server",
|
||||
IconUrl = "https://avatars0.githubusercontent.com/u/1363778?s=280&v=4",
|
||||
};
|
||||
var embed = new Embed
|
||||
{
|
||||
Author = new EmbedAuthor(assemblyInformationProvider.VersionPrefix)
|
||||
{
|
||||
Url = "https://github.com/tgstation/tgstation-server",
|
||||
IconUrl = "https://avatars0.githubusercontent.com/u/1363778?s=280&v=4",
|
||||
},
|
||||
Author = deploymentBranding ? author : default,
|
||||
Colour = Color.FromArgb(0xF1, 0xC4, 0x0F),
|
||||
Description = "TGS has begun deploying active repository code to production.",
|
||||
Fields = fields,
|
||||
|
||||
+1
-1
@@ -147,7 +147,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
public abstract Task MarkInactive(CompileJob compileJob, CancellationToken cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task<IReadOnlyCollection<RevInfoTestMerge>> RemoveMergedTestMerges(
|
||||
public abstract Task<IReadOnlyCollection<TestMerge>> RemoveMergedTestMerges(
|
||||
IRepository repository,
|
||||
RepositorySettings repositorySettings,
|
||||
RevisionInformation revisionInformation,
|
||||
|
||||
+4
-4
@@ -187,7 +187,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IReadOnlyCollection<RevInfoTestMerge>> RemoveMergedTestMerges(
|
||||
public override async Task<IReadOnlyCollection<TestMerge>> RemoveMergedTestMerges(
|
||||
IRepository repository,
|
||||
RepositorySettings repositorySettings,
|
||||
RevisionInformation revisionInformation,
|
||||
@@ -203,7 +203,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
if (revisionInformation.ActiveTestMerges?.Any() != true)
|
||||
{
|
||||
Logger.LogTrace("No test merges to remove.");
|
||||
return Array.Empty<RevInfoTestMerge>();
|
||||
return Array.Empty<TestMerge>();
|
||||
}
|
||||
|
||||
var gitHubClient = repositorySettings.AccessToken != null
|
||||
@@ -225,7 +225,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
Logger.LogWarning(ex, "Pull requests update check failed!");
|
||||
}
|
||||
|
||||
var newList = revisionInformation.ActiveTestMerges.ToList();
|
||||
var newList = revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList();
|
||||
|
||||
PullRequest lastMerged = null;
|
||||
async Task CheckRemovePR(Task<PullRequest> task)
|
||||
@@ -241,7 +241,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
lastMerged = pr;
|
||||
newList.Remove(
|
||||
newList.First(
|
||||
potential => potential.TestMerge.Number == pr.Number));
|
||||
potential => potential.Number == pr.Number));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -32,7 +32,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IReadOnlyCollection<RevInfoTestMerge>> RemoveMergedTestMerges(
|
||||
public override async Task<IReadOnlyCollection<TestMerge>> RemoveMergedTestMerges(
|
||||
IRepository repository,
|
||||
RepositorySettings repositorySettings,
|
||||
RevisionInformation revisionInformation,
|
||||
@@ -48,7 +48,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
if (revisionInformation.ActiveTestMerges?.Any() != true)
|
||||
{
|
||||
Logger.LogTrace("No test merges to remove.");
|
||||
return Array.Empty<RevInfoTestMerge>();
|
||||
return Array.Empty<TestMerge>();
|
||||
}
|
||||
|
||||
var client = repositorySettings.AccessToken != null
|
||||
@@ -72,7 +72,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
Logger.LogWarning(ex, "Merge requests update check failed!");
|
||||
}
|
||||
|
||||
var newList = revisionInformation.ActiveTestMerges.ToList();
|
||||
var newList = revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList();
|
||||
|
||||
MergeRequest lastMerged = null;
|
||||
async Task CheckRemoveMR(Task<MergeRequest> task)
|
||||
@@ -88,7 +88,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
lastMerged = mergeRequest;
|
||||
newList.Remove(
|
||||
newList.First(
|
||||
potential => potential.TestMerge.Number == mergeRequest.Id));
|
||||
potential => potential.Number == mergeRequest.Id));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,8 +85,8 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
/// <param name="repositorySettings">The <see cref="RepositorySettings"/>.</param>
|
||||
/// <param name="revisionInformation">The current <see cref="RevisionInformation"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IReadOnlyCollection{T}"/> of <see cref="RevInfoTestMerge"/>s that should remain the new <see cref="RevisionInformation"/>.</returns>
|
||||
Task<IReadOnlyCollection<RevInfoTestMerge>> RemoveMergedTestMerges(
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IReadOnlyCollection{T}"/> of <see cref="TestMerge"/>s that should remain the new <see cref="RevisionInformation"/>.</returns>
|
||||
Task<IReadOnlyCollection<TestMerge>> RemoveMergedTestMerges(
|
||||
IRepository repository,
|
||||
RepositorySettings repositorySettings,
|
||||
RevisionInformation revisionInformation,
|
||||
|
||||
+2
-2
@@ -40,11 +40,11 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyCollection<RevInfoTestMerge>> RemoveMergedTestMerges(
|
||||
public Task<IReadOnlyCollection<TestMerge>> RemoveMergedTestMerges(
|
||||
IRepository repository,
|
||||
RepositorySettings repositorySettings,
|
||||
RevisionInformation revisionInformation,
|
||||
CancellationToken cancellationToken) => Task.FromResult<IReadOnlyCollection<RevInfoTestMerge>>(Array.Empty<RevInfoTestMerge>());
|
||||
CancellationToken cancellationToken) => Task.FromResult<IReadOnlyCollection<TestMerge>>(Array.Empty<TestMerge>());
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StageDeployment(CompileJob compileJob, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
@@ -305,7 +305,7 @@ namespace Tgstation.Server.Host.Components
|
||||
var hasDbChanges = false;
|
||||
RevisionInformation currentRevInfo = null;
|
||||
Models.Instance attachedInstance = null;
|
||||
async Task UpdateRevInfo(string currentHead, bool onOrigin, IEnumerable<RevInfoTestMerge> updatedTestMerges)
|
||||
async Task UpdateRevInfo(string currentHead, bool onOrigin, IEnumerable<TestMerge> updatedTestMerges)
|
||||
{
|
||||
if (currentRevInfo == null)
|
||||
{
|
||||
@@ -351,8 +351,17 @@ namespace Tgstation.Server.Host.Components
|
||||
};
|
||||
|
||||
if (!onOrigin)
|
||||
currentRevInfo.ActiveTestMerges = new List<RevInfoTestMerge>(
|
||||
updatedTestMerges ?? oldRevInfo.ActiveTestMerges);
|
||||
{
|
||||
var testMerges = updatedTestMerges ?? oldRevInfo.ActiveTestMerges.Select(x => x.TestMerge);
|
||||
var revInfoTestMerges = testMerges.Select(
|
||||
testMerge => new RevInfoTestMerge
|
||||
{
|
||||
TestMerge = testMerge,
|
||||
})
|
||||
.ToList();
|
||||
|
||||
currentRevInfo.ActiveTestMerges = revInfoTestMerges;
|
||||
}
|
||||
|
||||
databaseContext.RevisionInformations.Add(currentRevInfo);
|
||||
hasDbChanges = true;
|
||||
@@ -381,8 +390,7 @@ namespace Tgstation.Server.Host.Components
|
||||
repo,
|
||||
repositorySettings,
|
||||
currentRevInfo,
|
||||
cancellationToken)
|
||||
;
|
||||
cancellationToken);
|
||||
|
||||
if (updatedTestMerges.Count == 0)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,578 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Request;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Repository
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for performing the complex <see cref="Job"/> of updating the repository on user request.
|
||||
/// </summary>
|
||||
sealed class RepositoryUpdateService
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="RepositoryUpdateRequest"/> for the <see cref="RepositoryUpdateService"/>.
|
||||
/// </summary>
|
||||
readonly RepositoryUpdateRequest model;
|
||||
|
||||
/// <summary>
|
||||
/// The current <see cref="RepositorySettings"/> for the <see cref="RepositoryUpdateService"/>.
|
||||
/// </summary>
|
||||
readonly RepositorySettings currentModel;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="User"/> that initiated the repository update.
|
||||
/// </summary>
|
||||
readonly User initiatingUser;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger{TCategoryName}"/> for the <see cref="RepositoryUpdateService"/>.
|
||||
/// </summary>
|
||||
readonly ILogger<RepositoryUpdateService> logger;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="EntityId.Id"/> of the associated <see cref="Instance"/>.
|
||||
/// </summary>
|
||||
readonly long instanceId;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RepositoryUpdateService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="model">The value of <see cref="model"/>.</param>
|
||||
/// <param name="currentModel">The value of <see cref="currentModel"/>.</param>
|
||||
/// <param name="initiatingUser">The value of <see cref="initiatingUser"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="logger"/>.</param>
|
||||
/// <param name="instanceId">The value of <see cref="instanceId"/>.</param>
|
||||
public RepositoryUpdateService(
|
||||
RepositoryUpdateRequest model,
|
||||
RepositorySettings currentModel,
|
||||
User initiatingUser,
|
||||
ILogger<RepositoryUpdateService> logger,
|
||||
long instanceId)
|
||||
{
|
||||
this.model = model ?? throw new ArgumentNullException(nameof(model));
|
||||
this.currentModel = currentModel ?? throw new ArgumentNullException(nameof(currentModel));
|
||||
this.initiatingUser = initiatingUser ?? throw new ArgumentNullException(nameof(initiatingUser));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
this.instanceId = instanceId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load the <see cref="RevisionInformation"/> for the current <paramref name="repository"/> state into a given <paramref name="databaseContext"/>.
|
||||
/// </summary>
|
||||
/// <param name="repository">The <see cref="IRepository"/>.</param>
|
||||
/// <param name="databaseContext">The active <see cref="IDatabaseContext"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> to use.</param>
|
||||
/// <param name="instance">The active <see cref="Models.Instance"/>.</param>
|
||||
/// <param name="lastOriginCommitSha">The last known origin commit SHA of the <paramref name="repository"/> if any.</param>
|
||||
/// <param name="revInfoSink">An optional <see cref="Action{T}"/> to receive the loaded <see cref="RevisionInformation"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the <paramref name="databaseContext"/> was modified in a way that requires saving, <see langword="false"/> otherwise.</returns>
|
||||
public static async Task<bool> LoadRevisionInformation(
|
||||
IRepository repository,
|
||||
IDatabaseContext databaseContext,
|
||||
ILogger logger,
|
||||
Models.Instance instance,
|
||||
string lastOriginCommitSha,
|
||||
Action<Models.RevisionInformation> revInfoSink,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var repoSha = repository.Head;
|
||||
|
||||
IQueryable<Models.RevisionInformation> ApplyQuery(IQueryable<Models.RevisionInformation> query) => query
|
||||
.Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id)
|
||||
.Include(x => x.CompileJobs)
|
||||
.Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy);
|
||||
|
||||
var revisionInfo = await ApplyQuery(databaseContext.RevisionInformations).FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
// If the DB doesn't have it, check the local set
|
||||
if (revisionInfo == default)
|
||||
revisionInfo = databaseContext
|
||||
.RevisionInformations
|
||||
.Local
|
||||
.Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id)
|
||||
.FirstOrDefault();
|
||||
|
||||
var needsDbUpdate = revisionInfo == default;
|
||||
if (needsDbUpdate)
|
||||
{
|
||||
// needs insertion
|
||||
revisionInfo = new Models.RevisionInformation
|
||||
{
|
||||
Instance = instance,
|
||||
CommitSha = repoSha,
|
||||
Timestamp = await repository.TimestampCommit(repoSha, cancellationToken),
|
||||
CompileJobs = new List<CompileJob>(),
|
||||
ActiveTestMerges = new List<RevInfoTestMerge>(), // non null vals for api returns
|
||||
};
|
||||
|
||||
lock (databaseContext) // cleaner this way
|
||||
databaseContext.RevisionInformations.Add(revisionInfo);
|
||||
}
|
||||
|
||||
revisionInfo.OriginCommitSha ??= lastOriginCommitSha;
|
||||
if (revisionInfo.OriginCommitSha == null)
|
||||
{
|
||||
revisionInfo.OriginCommitSha = repoSha;
|
||||
logger.LogInformation(Repository.OriginTrackingErrorTemplate, repoSha);
|
||||
}
|
||||
|
||||
revInfoSink?.Invoke(revisionInfo);
|
||||
return needsDbUpdate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The job entrypoint used by <see cref="Controllers.RepositoryController"/> to update the repository's current HEAD.
|
||||
/// </summary>
|
||||
/// <param name="instance">The <see cref="IInstanceCore"/> the job is running on. <see langword="null"/> only when performing an instance move operation.</param>
|
||||
/// <param name="databaseContextFactory">The <see cref="IDatabaseContextFactory"/> for the operation.</param>
|
||||
/// <param name="job">The running <see cref="Job"/>, ignored.</param>
|
||||
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> for the job.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
#pragma warning disable CA1502, CA1506 // TODO: Decomplexify
|
||||
public async Task<IActionResult> RepositoryUpdateJob(
|
||||
IInstanceCore instance,
|
||||
IDatabaseContextFactory databaseContextFactory,
|
||||
Job job,
|
||||
JobProgressReporter progressReporter,
|
||||
CancellationToken cancellationToken)
|
||||
#pragma warning restore CA1502, CA1506
|
||||
{
|
||||
var repoManager = instance.RepositoryManager;
|
||||
using var repo = await repoManager.LoadRepository(cancellationToken);
|
||||
if (repo == null)
|
||||
throw new JobException(ErrorCode.RepoMissing);
|
||||
|
||||
var modelHasShaOrReference = model.CheckoutSha != null || model.Reference != null;
|
||||
|
||||
var startReference = repo.Reference;
|
||||
var startSha = repo.Head;
|
||||
string postUpdateSha = null;
|
||||
|
||||
var newTestMerges = model.NewTestMerges != null && model.NewTestMerges.Count > 0;
|
||||
|
||||
if (newTestMerges && repo.RemoteGitProvider == RemoteGitProvider.Unknown)
|
||||
throw new JobException(ErrorCode.RepoUnsupportedTestMergeRemote);
|
||||
|
||||
var committerName = currentModel.ShowTestMergeCommitters.Value
|
||||
? initiatingUser.Name
|
||||
: currentModel.CommitterName;
|
||||
|
||||
var hardResettingToOriginReference = model.UpdateFromOrigin == true && model.Reference != null;
|
||||
|
||||
var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1));
|
||||
var progressFactor = 1.0 / numSteps;
|
||||
|
||||
JobProgressReporter NextProgressReporter(string stage)
|
||||
{
|
||||
return progressReporter.CreateSection(stage, progressFactor);
|
||||
}
|
||||
|
||||
progressReporter.ReportProgress(0);
|
||||
|
||||
// get a base line for where we are
|
||||
Models.RevisionInformation lastRevisionInfo = null;
|
||||
|
||||
var attachedInstance = new Models.Instance
|
||||
{
|
||||
Id = instanceId,
|
||||
};
|
||||
|
||||
Task CallLoadRevInfo(Models.TestMerge testMergeToAdd = null, string lastOriginCommitSha = null) => databaseContextFactory
|
||||
.UseContext(
|
||||
async databaseContext =>
|
||||
{
|
||||
databaseContext.Instances.Attach(attachedInstance);
|
||||
var previousRevInfo = lastRevisionInfo;
|
||||
var needsUpdate = await LoadRevisionInformation(
|
||||
repo,
|
||||
databaseContext,
|
||||
logger,
|
||||
attachedInstance,
|
||||
lastOriginCommitSha,
|
||||
x => lastRevisionInfo = x,
|
||||
cancellationToken);
|
||||
|
||||
if (testMergeToAdd != null)
|
||||
{
|
||||
// rev info may have already loaded the user
|
||||
var mergedBy = databaseContext.Users.Local.FirstOrDefault(x => x.Id == initiatingUser.Id);
|
||||
if (mergedBy == default)
|
||||
{
|
||||
mergedBy = new User
|
||||
{
|
||||
Id = initiatingUser.Id,
|
||||
};
|
||||
|
||||
databaseContext.Users.Attach(mergedBy);
|
||||
}
|
||||
|
||||
testMergeToAdd.MergedBy = mergedBy;
|
||||
testMergeToAdd.MergedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
foreach (var activeTestMerge in previousRevInfo.ActiveTestMerges)
|
||||
lastRevisionInfo.ActiveTestMerges.Add(activeTestMerge);
|
||||
|
||||
lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge
|
||||
{
|
||||
TestMerge = testMergeToAdd,
|
||||
});
|
||||
lastRevisionInfo.PrimaryTestMerge = testMergeToAdd;
|
||||
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
if (needsUpdate)
|
||||
await databaseContext.Save(cancellationToken);
|
||||
});
|
||||
|
||||
await CallLoadRevInfo();
|
||||
|
||||
// apply new rev info, tracking applied test merges
|
||||
Task UpdateRevInfo(Models.TestMerge testMergeToAdd = null) => CallLoadRevInfo(testMergeToAdd, lastRevisionInfo.OriginCommitSha);
|
||||
|
||||
try
|
||||
{
|
||||
// fetch/pull
|
||||
if (model.UpdateFromOrigin == true)
|
||||
{
|
||||
if (!repo.Tracking)
|
||||
throw new JobException(ErrorCode.RepoReferenceRequired);
|
||||
await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter("Fetch Origin"), cancellationToken);
|
||||
|
||||
if (!modelHasShaOrReference)
|
||||
{
|
||||
var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, NextProgressReporter("Merge Origin"), cancellationToken);
|
||||
if (!fastForward.HasValue)
|
||||
throw new JobException(ErrorCode.RepoMergeConflict);
|
||||
lastRevisionInfo.OriginCommitSha = await repo.GetOriginSha(cancellationToken);
|
||||
await UpdateRevInfo();
|
||||
if (fastForward.Value)
|
||||
{
|
||||
await repo.Sychronize(
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
currentModel.CommitterName,
|
||||
currentModel.CommitterEmail,
|
||||
NextProgressReporter("Sychronize"),
|
||||
true,
|
||||
cancellationToken)
|
||||
;
|
||||
postUpdateSha = repo.Head;
|
||||
}
|
||||
else
|
||||
NextProgressReporter(null).ReportProgress(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
var updateSubmodules = currentModel.UpdateSubmodules.Value;
|
||||
|
||||
// checkout/hard reset
|
||||
if (modelHasShaOrReference)
|
||||
{
|
||||
var validCheckoutSha =
|
||||
model.CheckoutSha != null
|
||||
&& !repo.Head.StartsWith(model.CheckoutSha, StringComparison.OrdinalIgnoreCase);
|
||||
var validCheckoutReference =
|
||||
model.Reference != null
|
||||
&& !repo.Reference.Equals(model.Reference, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (validCheckoutSha || validCheckoutReference)
|
||||
{
|
||||
var committish = model.CheckoutSha ?? model.Reference;
|
||||
var isSha = await repo.IsSha(committish, cancellationToken);
|
||||
|
||||
if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null))
|
||||
throw new JobException(ErrorCode.RepoSwappedShaOrReference);
|
||||
|
||||
await repo.CheckoutObject(
|
||||
committish,
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
updateSubmodules,
|
||||
NextProgressReporter("Checkout"),
|
||||
cancellationToken)
|
||||
;
|
||||
await CallLoadRevInfo(); // we've either seen origin before or what we're checking out is on origin
|
||||
}
|
||||
else
|
||||
NextProgressReporter(null).ReportProgress(1.0);
|
||||
|
||||
if (hardResettingToOriginReference)
|
||||
{
|
||||
if (!repo.Tracking)
|
||||
throw new JobException(ErrorCode.RepoReferenceNotTracking);
|
||||
await repo.ResetToOrigin(
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
updateSubmodules,
|
||||
NextProgressReporter("Reset to Origin"),
|
||||
cancellationToken)
|
||||
;
|
||||
await repo.Sychronize(
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
currentModel.CommitterName,
|
||||
currentModel.CommitterEmail,
|
||||
NextProgressReporter("Synchronize"),
|
||||
true,
|
||||
cancellationToken)
|
||||
;
|
||||
await CallLoadRevInfo();
|
||||
|
||||
// repo head is on origin so force this
|
||||
// will update the db if necessary
|
||||
lastRevisionInfo.OriginCommitSha = repo.Head;
|
||||
}
|
||||
}
|
||||
|
||||
// test merging
|
||||
if (newTestMerges)
|
||||
{
|
||||
if (repo.RemoteGitProvider == RemoteGitProvider.Unknown)
|
||||
throw new JobException(ErrorCode.RepoTestMergeInvalidRemote);
|
||||
|
||||
// bit of sanitization
|
||||
foreach (var newTestMergeWithoutTargetCommitSha in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.TargetCommitSha)))
|
||||
newTestMergeWithoutTargetCommitSha.TargetCommitSha = null;
|
||||
|
||||
var repoOwner = repo.RemoteRepositoryOwner;
|
||||
var repoName = repo.RemoteRepositoryName;
|
||||
|
||||
// optimization: if we've already merged these exact same commits in this fashion before, just find the rev info for it and check it out
|
||||
Models.RevisionInformation revInfoWereLookingFor = null;
|
||||
bool needToApplyRemainingPrs = true;
|
||||
if (lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha)
|
||||
{
|
||||
bool cantSearch = false;
|
||||
foreach (var newTestMerge in model.NewTestMerges)
|
||||
{
|
||||
if (newTestMerge.TargetCommitSha != null)
|
||||
#pragma warning disable CA1308 // Normalize strings to uppercase
|
||||
newTestMerge.TargetCommitSha = newTestMerge.TargetCommitSha?.ToLowerInvariant(); // ala libgit2
|
||||
#pragma warning restore CA1308 // Normalize strings to uppercase
|
||||
else
|
||||
try
|
||||
{
|
||||
// retrieve the latest sha
|
||||
var pr = await repo.GetTestMerge(newTestMerge, currentModel, cancellationToken);
|
||||
|
||||
// we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it
|
||||
newTestMerge.TargetCommitSha = pr.TargetCommitSha;
|
||||
}
|
||||
catch
|
||||
{
|
||||
cantSearch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cantSearch)
|
||||
{
|
||||
List<Models.RevisionInformation> dbPull = null;
|
||||
|
||||
await databaseContextFactory.UseContext(
|
||||
async databaseContext =>
|
||||
dbPull = await databaseContext.RevisionInformations
|
||||
.AsQueryable()
|
||||
.Where(x => x.Instance.Id == instanceId
|
||||
&& x.OriginCommitSha == lastRevisionInfo.OriginCommitSha
|
||||
&& x.ActiveTestMerges.Count <= model.NewTestMerges.Count
|
||||
&& x.ActiveTestMerges.Count > 0)
|
||||
.Include(x => x.ActiveTestMerges)
|
||||
.ThenInclude(x => x.TestMerge)
|
||||
.ToListAsync(cancellationToken));
|
||||
|
||||
// split here cause this bit has to be done locally
|
||||
revInfoWereLookingFor = dbPull
|
||||
.Where(x => x.ActiveTestMerges.Count == model.NewTestMerges.Count
|
||||
&& x.ActiveTestMerges.Select(y => y.TestMerge)
|
||||
.All(y => model.NewTestMerges.Any(z =>
|
||||
y.Number == z.Number
|
||||
&& y.TargetCommitSha.StartsWith(z.TargetCommitSha, StringComparison.Ordinal)
|
||||
&& (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null))))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (revInfoWereLookingFor == default && model.NewTestMerges.Count > 1)
|
||||
{
|
||||
// okay try to add at least SOME prs we've seen before
|
||||
var listedNewTestMerges = model.NewTestMerges.ToList();
|
||||
|
||||
var appliedTestMergeIds = new List<long>();
|
||||
|
||||
Models.RevisionInformation lastGoodRevInfo = null;
|
||||
do
|
||||
{
|
||||
foreach (var newTestMergeParameters in listedNewTestMerges)
|
||||
{
|
||||
revInfoWereLookingFor = dbPull
|
||||
.Where(testRevInfo =>
|
||||
{
|
||||
if (testRevInfo.PrimaryTestMerge == null)
|
||||
return false;
|
||||
|
||||
var testMergeMatch = model.NewTestMerges.Any(testTestMerge =>
|
||||
{
|
||||
var numberMatch = testRevInfo.PrimaryTestMerge.Number == testTestMerge.Number;
|
||||
if (!numberMatch)
|
||||
return false;
|
||||
|
||||
var shaMatch = testRevInfo.PrimaryTestMerge.TargetCommitSha.StartsWith(
|
||||
testTestMerge.TargetCommitSha,
|
||||
StringComparison.Ordinal);
|
||||
if (!shaMatch)
|
||||
return false;
|
||||
|
||||
var commentMatch = testRevInfo.PrimaryTestMerge.Comment == testTestMerge.Comment;
|
||||
return commentMatch;
|
||||
});
|
||||
|
||||
if (!testMergeMatch)
|
||||
return false;
|
||||
|
||||
var previousTestMergesMatch = testRevInfo
|
||||
.ActiveTestMerges
|
||||
.Select(previousRevInfoTestMerge => previousRevInfoTestMerge.TestMerge)
|
||||
.All(previousTestMerge => appliedTestMergeIds.Contains(previousTestMerge.Id));
|
||||
|
||||
return previousTestMergesMatch;
|
||||
})
|
||||
.FirstOrDefault();
|
||||
|
||||
if (revInfoWereLookingFor != null)
|
||||
{
|
||||
lastGoodRevInfo = revInfoWereLookingFor;
|
||||
appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge.Id);
|
||||
listedNewTestMerges.Remove(newTestMergeParameters);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (revInfoWereLookingFor != null && listedNewTestMerges.Count > 0);
|
||||
|
||||
revInfoWereLookingFor = lastGoodRevInfo;
|
||||
needToApplyRemainingPrs = listedNewTestMerges.Count != 0;
|
||||
if (needToApplyRemainingPrs)
|
||||
model.NewTestMerges = listedNewTestMerges;
|
||||
}
|
||||
else if (revInfoWereLookingFor != null)
|
||||
needToApplyRemainingPrs = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (revInfoWereLookingFor != null)
|
||||
{
|
||||
// goteem
|
||||
logger.LogDebug("Reusing existing SHA {0}...", revInfoWereLookingFor.CommitSha);
|
||||
await repo.ResetToSha(revInfoWereLookingFor.CommitSha, NextProgressReporter($"Reset to {revInfoWereLookingFor.CommitSha[..7]}"), cancellationToken);
|
||||
lastRevisionInfo = revInfoWereLookingFor;
|
||||
}
|
||||
|
||||
if (needToApplyRemainingPrs)
|
||||
{
|
||||
foreach (var newTestMerge in model.NewTestMerges)
|
||||
{
|
||||
if (lastRevisionInfo.ActiveTestMerges.Any(x => x.TestMerge.Number == newTestMerge.Number))
|
||||
throw new JobException(ErrorCode.RepoDuplicateTestMerge);
|
||||
|
||||
var fullTestMergeTask = repo.GetTestMerge(newTestMerge, currentModel, cancellationToken);
|
||||
|
||||
var mergeResult = await repo.AddTestMerge(
|
||||
newTestMerge,
|
||||
committerName,
|
||||
currentModel.CommitterEmail,
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
updateSubmodules,
|
||||
NextProgressReporter($"Test merge #{newTestMerge.Number}"),
|
||||
cancellationToken);
|
||||
|
||||
if (mergeResult == null)
|
||||
throw new JobException(
|
||||
ErrorCode.RepoTestMergeConflict,
|
||||
new JobException(
|
||||
$"Test Merge #{newTestMerge.Number} at {newTestMerge.TargetCommitSha[..7]} conflicted!"));
|
||||
|
||||
Models.TestMerge fullTestMerge;
|
||||
try
|
||||
{
|
||||
fullTestMerge = await fullTestMergeTask;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning("Error retrieving metadata for test merge #{testMergeNumber}!", newTestMerge.Number);
|
||||
|
||||
fullTestMerge = new Models.TestMerge
|
||||
{
|
||||
Author = ex.Message,
|
||||
BodyAtMerge = ex.Message,
|
||||
TitleAtMerge = ex.Message,
|
||||
Comment = newTestMerge.Comment,
|
||||
Number = newTestMerge.Number,
|
||||
Url = ex.Message,
|
||||
};
|
||||
}
|
||||
|
||||
// Ensure we're getting the full sha from git itself
|
||||
fullTestMerge.TargetCommitSha = newTestMerge.TargetCommitSha;
|
||||
|
||||
await UpdateRevInfo(fullTestMerge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var currentHead = repo.Head;
|
||||
if (currentModel.PushTestMergeCommits.Value && (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead)))
|
||||
{
|
||||
await repo.Sychronize(
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
currentModel.CommitterName,
|
||||
currentModel.CommitterEmail,
|
||||
NextProgressReporter("Synchronize"),
|
||||
false,
|
||||
cancellationToken)
|
||||
;
|
||||
await UpdateRevInfo();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
numSteps = 2;
|
||||
|
||||
// Forget what we've done and abort
|
||||
progressReporter.ReportProgress(0.0);
|
||||
|
||||
var secondStep = startReference != null && repo.Head != startSha;
|
||||
|
||||
// DCTx2: Cancellation token is for job, operations should always run
|
||||
await repo.CheckoutObject(
|
||||
startReference ?? startSha,
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
true,
|
||||
progressReporter.CreateSection($"Checkout {startReference ?? startSha[..7]}", secondStep ? 0.5 : 1.0),
|
||||
default);
|
||||
|
||||
if (secondStep)
|
||||
await repo.ResetToSha(startSha, progressReporter.CreateSection($"Hard reset to SHA {startSha[..7]}", 0.5), default);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
@@ -17,7 +16,7 @@ using Tgstation.Server.Api.Models.Request;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
using Tgstation.Server.Host.Components;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Components.Repository;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Models;
|
||||
@@ -33,9 +32,9 @@ namespace Tgstation.Server.Host.Controllers
|
||||
public sealed class RepositoryController : InstanceRequiredController
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IGitHubClientFactory"/> for the <see cref="RepositoryController"/>.
|
||||
/// The <see cref="ILoggerFactory"/> for the <see cref="RepositoryController"/>.
|
||||
/// </summary>
|
||||
readonly IGitHubClientFactory gitHubClientFactory;
|
||||
readonly ILoggerFactory loggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IJobManager"/> for the <see cref="RepositoryController"/>.
|
||||
@@ -48,14 +47,14 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="gitHubClientFactory">The value of <see cref="gitHubClientFactory"/>.</param>
|
||||
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/>.</param>
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
public RepositoryController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IInstanceManager instanceManager,
|
||||
IGitHubClientFactory gitHubClientFactory,
|
||||
ILoggerFactory loggerFactory,
|
||||
IJobManager jobManager,
|
||||
ILogger<RepositoryController> logger)
|
||||
: base(
|
||||
@@ -64,7 +63,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
authenticationContextFactory,
|
||||
logger)
|
||||
{
|
||||
this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
|
||||
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
|
||||
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
|
||||
}
|
||||
|
||||
@@ -315,8 +314,9 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[ProducesResponseType(typeof(RepositoryResponse), 200)]
|
||||
[ProducesResponseType(typeof(RepositoryResponse), 202)]
|
||||
[ProducesResponseType(typeof(ErrorMessageResponse), 410)]
|
||||
#pragma warning disable CA1502, CA1505 // TODO: Decomplexify
|
||||
#pragma warning disable CA1502 // TODO: Decomplexify
|
||||
public async Task<IActionResult> Update([FromBody] RepositoryUpdateRequest model, CancellationToken cancellationToken)
|
||||
#pragma warning restore CA1502
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
@@ -462,439 +462,6 @@ namespace Tgstation.Server.Host.Controllers
|
||||
if (description == null)
|
||||
return Json(api); // no git changes
|
||||
|
||||
async Task<IActionResult> RepositoryUpdateJobOhGodPleaseSomeoneRefactorThisItsTooFuckingBig(
|
||||
IInstanceCore instance,
|
||||
IDatabaseContextFactory databaseContextFactory,
|
||||
JobProgressReporter progressReporter,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var repoManager = instance.RepositoryManager;
|
||||
using var repo = await repoManager.LoadRepository(ct);
|
||||
if (repo == null)
|
||||
throw new JobException(ErrorCode.RepoMissing);
|
||||
|
||||
var modelHasShaOrReference = model.CheckoutSha != null || model.Reference != null;
|
||||
|
||||
var startReference = repo.Reference;
|
||||
var startSha = repo.Head;
|
||||
string postUpdateSha = null;
|
||||
|
||||
if (newTestMerges && repo.RemoteGitProvider == RemoteGitProvider.Unknown)
|
||||
throw new JobException(ErrorCode.RepoUnsupportedTestMergeRemote);
|
||||
|
||||
var committerName = currentModel.ShowTestMergeCommitters.Value
|
||||
? AuthenticationContext.User.Name
|
||||
: currentModel.CommitterName;
|
||||
|
||||
var hardResettingToOriginReference = model.UpdateFromOrigin == true && model.Reference != null;
|
||||
|
||||
var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1));
|
||||
var progressFactor = 1.0 / numSteps;
|
||||
|
||||
JobProgressReporter NextProgressReporter(string stage)
|
||||
{
|
||||
return progressReporter.CreateSection(stage, progressFactor);
|
||||
}
|
||||
|
||||
progressReporter.ReportProgress(0);
|
||||
|
||||
// get a base line for where we are
|
||||
Models.RevisionInformation lastRevisionInfo = null;
|
||||
|
||||
var attachedInstance = new Models.Instance
|
||||
{
|
||||
Id = Instance.Id,
|
||||
};
|
||||
|
||||
Task CallLoadRevInfo(Models.TestMerge testMergeToAdd = null, string lastOriginCommitSha = null) => databaseContextFactory
|
||||
.UseContext(
|
||||
async databaseContext =>
|
||||
{
|
||||
databaseContext.Instances.Attach(attachedInstance);
|
||||
var previousRevInfo = lastRevisionInfo;
|
||||
var needsUpdate = await LoadRevisionInformation(
|
||||
repo,
|
||||
databaseContext,
|
||||
attachedInstance,
|
||||
lastOriginCommitSha,
|
||||
x => lastRevisionInfo = x,
|
||||
ct)
|
||||
;
|
||||
|
||||
if (testMergeToAdd != null)
|
||||
{
|
||||
// rev info may have already loaded the user
|
||||
var mergedBy = databaseContext.Users.Local.FirstOrDefault(x => x.Id == AuthenticationContext.User.Id);
|
||||
if (mergedBy == default)
|
||||
{
|
||||
mergedBy = new User
|
||||
{
|
||||
Id = AuthenticationContext.User.Id,
|
||||
};
|
||||
|
||||
databaseContext.Users.Attach(mergedBy);
|
||||
}
|
||||
|
||||
testMergeToAdd.MergedBy = mergedBy;
|
||||
testMergeToAdd.MergedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
foreach (var activeTestMerge in previousRevInfo.ActiveTestMerges)
|
||||
lastRevisionInfo.ActiveTestMerges.Add(activeTestMerge);
|
||||
|
||||
lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge
|
||||
{
|
||||
TestMerge = testMergeToAdd,
|
||||
});
|
||||
lastRevisionInfo.PrimaryTestMerge = testMergeToAdd;
|
||||
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
if (needsUpdate)
|
||||
await databaseContext.Save(ct);
|
||||
});
|
||||
|
||||
await CallLoadRevInfo();
|
||||
|
||||
// apply new rev info, tracking applied test merges
|
||||
Task UpdateRevInfo(Models.TestMerge testMergeToAdd = null) => CallLoadRevInfo(testMergeToAdd, lastRevisionInfo.OriginCommitSha);
|
||||
|
||||
try
|
||||
{
|
||||
// fetch/pull
|
||||
if (model.UpdateFromOrigin == true)
|
||||
{
|
||||
if (!repo.Tracking)
|
||||
throw new JobException(ErrorCode.RepoReferenceRequired);
|
||||
await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter("Fetch Origin"), ct);
|
||||
|
||||
if (!modelHasShaOrReference)
|
||||
{
|
||||
var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, NextProgressReporter("Merge Origin"), ct);
|
||||
if (!fastForward.HasValue)
|
||||
throw new JobException(ErrorCode.RepoMergeConflict);
|
||||
lastRevisionInfo.OriginCommitSha = await repo.GetOriginSha(ct);
|
||||
await UpdateRevInfo();
|
||||
if (fastForward.Value)
|
||||
{
|
||||
await repo.Sychronize(
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
currentModel.CommitterName,
|
||||
currentModel.CommitterEmail,
|
||||
NextProgressReporter("Sychronize"),
|
||||
true,
|
||||
ct)
|
||||
;
|
||||
postUpdateSha = repo.Head;
|
||||
}
|
||||
else
|
||||
NextProgressReporter(null).ReportProgress(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
var updateSubmodules = currentModel.UpdateSubmodules.Value;
|
||||
|
||||
// checkout/hard reset
|
||||
if (modelHasShaOrReference)
|
||||
{
|
||||
var validCheckoutSha =
|
||||
model.CheckoutSha != null
|
||||
&& !repo.Head.StartsWith(model.CheckoutSha, StringComparison.OrdinalIgnoreCase);
|
||||
var validCheckoutReference =
|
||||
model.Reference != null
|
||||
&& !repo.Reference.Equals(model.Reference, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (validCheckoutSha || validCheckoutReference)
|
||||
{
|
||||
var committish = model.CheckoutSha ?? model.Reference;
|
||||
var isSha = await repo.IsSha(committish, ct);
|
||||
|
||||
if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null))
|
||||
throw new JobException(ErrorCode.RepoSwappedShaOrReference);
|
||||
|
||||
await repo.CheckoutObject(
|
||||
committish,
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
updateSubmodules,
|
||||
NextProgressReporter("Checkout"),
|
||||
ct)
|
||||
;
|
||||
await CallLoadRevInfo(); // we've either seen origin before or what we're checking out is on origin
|
||||
}
|
||||
else
|
||||
NextProgressReporter(null).ReportProgress(1.0);
|
||||
|
||||
if (hardResettingToOriginReference)
|
||||
{
|
||||
if (!repo.Tracking)
|
||||
throw new JobException(ErrorCode.RepoReferenceNotTracking);
|
||||
await repo.ResetToOrigin(
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
updateSubmodules,
|
||||
NextProgressReporter("Reset to Origin"),
|
||||
ct)
|
||||
;
|
||||
await repo.Sychronize(
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
currentModel.CommitterName,
|
||||
currentModel.CommitterEmail,
|
||||
NextProgressReporter("Synchronize"),
|
||||
true,
|
||||
ct)
|
||||
;
|
||||
await CallLoadRevInfo();
|
||||
|
||||
// repo head is on origin so force this
|
||||
// will update the db if necessary
|
||||
lastRevisionInfo.OriginCommitSha = repo.Head;
|
||||
}
|
||||
}
|
||||
|
||||
// test merging
|
||||
if (newTestMerges)
|
||||
{
|
||||
if (repo.RemoteGitProvider == RemoteGitProvider.Unknown)
|
||||
throw new JobException(ErrorCode.RepoTestMergeInvalidRemote);
|
||||
|
||||
// bit of sanitization
|
||||
foreach (var newTestMergeWithoutTargetCommitSha in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.TargetCommitSha)))
|
||||
newTestMergeWithoutTargetCommitSha.TargetCommitSha = null;
|
||||
|
||||
var gitHubClient = currentModel.AccessToken != null
|
||||
? gitHubClientFactory.CreateClient(currentModel.AccessToken)
|
||||
: gitHubClientFactory.CreateClient();
|
||||
|
||||
var repoOwner = repo.RemoteRepositoryOwner;
|
||||
var repoName = repo.RemoteRepositoryName;
|
||||
|
||||
// optimization: if we've already merged these exact same commits in this fashion before, just find the rev info for it and check it out
|
||||
Models.RevisionInformation revInfoWereLookingFor = null;
|
||||
bool needToApplyRemainingPrs = true;
|
||||
if (lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha)
|
||||
{
|
||||
bool cantSearch = false;
|
||||
foreach (var newTestMerge in model.NewTestMerges)
|
||||
{
|
||||
if (newTestMerge.TargetCommitSha != null)
|
||||
#pragma warning disable CA1308 // Normalize strings to uppercase
|
||||
newTestMerge.TargetCommitSha = newTestMerge.TargetCommitSha?.ToLowerInvariant(); // ala libgit2
|
||||
#pragma warning restore CA1308 // Normalize strings to uppercase
|
||||
else
|
||||
try
|
||||
{
|
||||
// retrieve the latest sha
|
||||
var pr = await repo.GetTestMerge(newTestMerge, currentModel, ct);
|
||||
|
||||
// we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it
|
||||
newTestMerge.TargetCommitSha = pr.TargetCommitSha;
|
||||
}
|
||||
catch
|
||||
{
|
||||
cantSearch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cantSearch)
|
||||
{
|
||||
List<Models.RevisionInformation> dbPull = null;
|
||||
|
||||
await databaseContextFactory.UseContext(
|
||||
async databaseContext =>
|
||||
dbPull = await databaseContext.RevisionInformations
|
||||
.AsQueryable()
|
||||
.Where(x => x.Instance.Id == Instance.Id
|
||||
&& x.OriginCommitSha == lastRevisionInfo.OriginCommitSha
|
||||
&& x.ActiveTestMerges.Count <= model.NewTestMerges.Count
|
||||
&& x.ActiveTestMerges.Count > 0)
|
||||
.Include(x => x.ActiveTestMerges)
|
||||
.ThenInclude(x => x.TestMerge)
|
||||
.ToListAsync(ct));
|
||||
|
||||
// split here cause this bit has to be done locally
|
||||
revInfoWereLookingFor = dbPull
|
||||
.Where(x => x.ActiveTestMerges.Count == model.NewTestMerges.Count
|
||||
&& x.ActiveTestMerges.Select(y => y.TestMerge)
|
||||
.All(y => model.NewTestMerges.Any(z =>
|
||||
y.Number == z.Number
|
||||
&& y.TargetCommitSha.StartsWith(z.TargetCommitSha, StringComparison.Ordinal)
|
||||
&& (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null))))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (revInfoWereLookingFor == default && model.NewTestMerges.Count > 1)
|
||||
{
|
||||
// okay try to add at least SOME prs we've seen before
|
||||
var listedNewTestMerges = model.NewTestMerges.ToList();
|
||||
|
||||
var appliedTestMergeIds = new List<long>();
|
||||
|
||||
Models.RevisionInformation lastGoodRevInfo = null;
|
||||
do
|
||||
{
|
||||
foreach (var newTestMergeParameters in listedNewTestMerges)
|
||||
{
|
||||
revInfoWereLookingFor = dbPull
|
||||
.Where(testRevInfo =>
|
||||
{
|
||||
if (testRevInfo.PrimaryTestMerge == null)
|
||||
return false;
|
||||
|
||||
var testMergeMatch = model.NewTestMerges.Any(testTestMerge =>
|
||||
{
|
||||
var numberMatch = testRevInfo.PrimaryTestMerge.Number == testTestMerge.Number;
|
||||
if (!numberMatch)
|
||||
return false;
|
||||
|
||||
var shaMatch = testRevInfo.PrimaryTestMerge.TargetCommitSha.StartsWith(
|
||||
testTestMerge.TargetCommitSha,
|
||||
StringComparison.Ordinal);
|
||||
if (!shaMatch)
|
||||
return false;
|
||||
|
||||
var commentMatch = testRevInfo.PrimaryTestMerge.Comment == testTestMerge.Comment;
|
||||
return commentMatch;
|
||||
});
|
||||
|
||||
if (!testMergeMatch)
|
||||
return false;
|
||||
|
||||
var previousTestMergesMatch = testRevInfo
|
||||
.ActiveTestMerges
|
||||
.Select(previousRevInfoTestMerge => previousRevInfoTestMerge.TestMerge)
|
||||
.All(previousTestMerge => appliedTestMergeIds.Contains(previousTestMerge.Id));
|
||||
|
||||
return previousTestMergesMatch;
|
||||
})
|
||||
.FirstOrDefault();
|
||||
|
||||
if (revInfoWereLookingFor != null)
|
||||
{
|
||||
lastGoodRevInfo = revInfoWereLookingFor;
|
||||
appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge.Id);
|
||||
listedNewTestMerges.Remove(newTestMergeParameters);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (revInfoWereLookingFor != null && listedNewTestMerges.Count > 0);
|
||||
|
||||
revInfoWereLookingFor = lastGoodRevInfo;
|
||||
needToApplyRemainingPrs = listedNewTestMerges.Count != 0;
|
||||
if (needToApplyRemainingPrs)
|
||||
model.NewTestMerges = listedNewTestMerges;
|
||||
}
|
||||
else if (revInfoWereLookingFor != null)
|
||||
needToApplyRemainingPrs = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (revInfoWereLookingFor != null)
|
||||
{
|
||||
// goteem
|
||||
Logger.LogDebug("Reusing existing SHA {0}...", revInfoWereLookingFor.CommitSha);
|
||||
await repo.ResetToSha(revInfoWereLookingFor.CommitSha, NextProgressReporter($"Reset to {revInfoWereLookingFor.CommitSha[..7]}"), ct);
|
||||
lastRevisionInfo = revInfoWereLookingFor;
|
||||
}
|
||||
|
||||
if (needToApplyRemainingPrs)
|
||||
{
|
||||
foreach (var newTestMerge in model.NewTestMerges)
|
||||
{
|
||||
if (lastRevisionInfo.ActiveTestMerges.Any(x => x.TestMerge.Number == newTestMerge.Number))
|
||||
throw new JobException(ErrorCode.RepoDuplicateTestMerge);
|
||||
|
||||
var fullTestMergeTask = repo.GetTestMerge(newTestMerge, currentModel, ct);
|
||||
|
||||
var mergeResult = await repo.AddTestMerge(
|
||||
newTestMerge,
|
||||
committerName,
|
||||
currentModel.CommitterEmail,
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
updateSubmodules,
|
||||
NextProgressReporter($"Test merge #{newTestMerge.Number}"),
|
||||
ct);
|
||||
|
||||
if (mergeResult == null)
|
||||
throw new JobException(
|
||||
ErrorCode.RepoTestMergeConflict,
|
||||
new JobException(
|
||||
$"Test Merge #{newTestMerge.Number} at {newTestMerge.TargetCommitSha[..7]} conflicted!"));
|
||||
|
||||
Models.TestMerge fullTestMerge;
|
||||
try
|
||||
{
|
||||
fullTestMerge = await fullTestMergeTask;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning("Error retrieving metadata for test merge #{testMergeNumber}!", newTestMerge.Number);
|
||||
|
||||
fullTestMerge = new Models.TestMerge
|
||||
{
|
||||
Author = ex.Message,
|
||||
BodyAtMerge = ex.Message,
|
||||
TitleAtMerge = ex.Message,
|
||||
Comment = newTestMerge.Comment,
|
||||
Number = newTestMerge.Number,
|
||||
Url = ex.Message,
|
||||
};
|
||||
}
|
||||
|
||||
// Ensure we're getting the full sha from git itself
|
||||
fullTestMerge.TargetCommitSha = newTestMerge.TargetCommitSha;
|
||||
|
||||
await UpdateRevInfo(fullTestMerge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var currentHead = repo.Head;
|
||||
if (currentModel.PushTestMergeCommits.Value && (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead)))
|
||||
{
|
||||
await repo.Sychronize(
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
currentModel.CommitterName,
|
||||
currentModel.CommitterEmail,
|
||||
NextProgressReporter("Synchronize"),
|
||||
false,
|
||||
ct)
|
||||
;
|
||||
await UpdateRevInfo();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
numSteps = 2;
|
||||
|
||||
// Forget what we've done and abort
|
||||
progressReporter.ReportProgress(0.0);
|
||||
|
||||
var secondStep = startReference != null && repo.Head != startSha;
|
||||
|
||||
// DCTx2: Cancellation token is for job, operations should always run
|
||||
await repo.CheckoutObject(
|
||||
startReference ?? startSha,
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
true,
|
||||
progressReporter.CreateSection($"Checkout {startReference ?? startSha[..7]}", secondStep ? 0.5 : 1.0),
|
||||
default);
|
||||
|
||||
if (secondStep)
|
||||
await repo.ResetToSha(startSha, progressReporter.CreateSection($"Hard reset to SHA {startSha[..7]}", 0.5), default);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
var job = new Job
|
||||
{
|
||||
Description = description,
|
||||
@@ -904,97 +471,35 @@ namespace Tgstation.Server.Host.Controllers
|
||||
CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
|
||||
};
|
||||
|
||||
var repositoryUpdater = new RepositoryUpdateService(
|
||||
model,
|
||||
currentModel,
|
||||
AuthenticationContext.User,
|
||||
loggerFactory.CreateLogger<RepositoryUpdateService>(),
|
||||
Instance.Id.Value);
|
||||
|
||||
// Time to access git, do it in a job
|
||||
await jobManager.RegisterOperation(
|
||||
job,
|
||||
(core, databaseContextFactory, paramJob, progressReporter, ct) =>
|
||||
RepositoryUpdateJobOhGodPleaseSomeoneRefactorThisItsTooFuckingBig(
|
||||
core,
|
||||
databaseContextFactory,
|
||||
progressReporter,
|
||||
ct),
|
||||
repositoryUpdater.RepositoryUpdateJob,
|
||||
cancellationToken);
|
||||
|
||||
api.ActiveJob = job.ToApi();
|
||||
return Accepted(api);
|
||||
}
|
||||
#pragma warning restore CA1502, CA1505
|
||||
|
||||
/// <summary>
|
||||
/// Load the <see cref="RevisionInformation"/> for the current <paramref name="repository"/> state into a given <paramref name="databaseContext"/>.
|
||||
/// </summary>
|
||||
/// <param name="repository">The <see cref="Components.Repository.IRepository"/>.</param>
|
||||
/// <param name="databaseContext">The active <see cref="IDatabaseContext"/>.</param>
|
||||
/// <param name="instance">The active <see cref="Models.Instance"/>.</param>
|
||||
/// <param name="lastOriginCommitSha">The last known origin commit SHA of the <paramref name="repository"/> if any.</param>
|
||||
/// <param name="revInfoSink">An optional <see cref="Action{T}"/> to receive the loaded <see cref="RevisionInformation"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the <paramref name="databaseContext"/> was modified in a way that requires saving, <see langword="false"/> otherwise.</returns>
|
||||
async Task<bool> LoadRevisionInformation(
|
||||
Components.Repository.IRepository repository,
|
||||
IDatabaseContext databaseContext,
|
||||
Models.Instance instance,
|
||||
string lastOriginCommitSha,
|
||||
Action<Models.RevisionInformation> revInfoSink,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var repoSha = repository.Head;
|
||||
|
||||
IQueryable<Models.RevisionInformation> ApplyQuery(IQueryable<Models.RevisionInformation> query) => query
|
||||
.Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id)
|
||||
.Include(x => x.CompileJobs)
|
||||
.Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy);
|
||||
|
||||
var revisionInfo = await ApplyQuery(databaseContext.RevisionInformations).FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
// If the DB doesn't have it, check the local set
|
||||
if (revisionInfo == default)
|
||||
revisionInfo = databaseContext
|
||||
.RevisionInformations
|
||||
.Local
|
||||
.Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id)
|
||||
.FirstOrDefault();
|
||||
|
||||
var needsDbUpdate = revisionInfo == default;
|
||||
if (needsDbUpdate)
|
||||
{
|
||||
// needs insertion
|
||||
revisionInfo = new Models.RevisionInformation
|
||||
{
|
||||
Instance = instance,
|
||||
CommitSha = repoSha,
|
||||
Timestamp = await repository.TimestampCommit(repoSha, cancellationToken),
|
||||
CompileJobs = new List<CompileJob>(),
|
||||
ActiveTestMerges = new List<RevInfoTestMerge>(), // non null vals for api returns
|
||||
};
|
||||
|
||||
lock (databaseContext) // cleaner this way
|
||||
databaseContext.RevisionInformations.Add(revisionInfo);
|
||||
}
|
||||
|
||||
revisionInfo.OriginCommitSha ??= lastOriginCommitSha;
|
||||
if (revisionInfo.OriginCommitSha == null)
|
||||
{
|
||||
revisionInfo.OriginCommitSha = repoSha;
|
||||
Logger.LogInformation(Components.Repository.Repository.OriginTrackingErrorTemplate, repoSha);
|
||||
}
|
||||
|
||||
revInfoSink?.Invoke(revisionInfo);
|
||||
return needsDbUpdate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populate a given <paramref name="apiResponse"/> with the current state of a given <paramref name="repository"/>.
|
||||
/// </summary>
|
||||
/// <param name="apiResponse">The <see cref="RepositoryResponse"/> to populate.</param>
|
||||
/// <param name="repository">The <see cref="Components.Repository.IRepository"/>.</param>
|
||||
/// <param name="repository">The <see cref="IRepository"/>.</param>
|
||||
/// <param name="databaseContext">The active <see cref="IDatabaseContext"/>.</param>
|
||||
/// <param name="instance">The active <see cref="Models.Instance"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the <paramref name="databaseContext"/> was modified in a way that requires saving, <see langword="false"/> otherwise.</returns>
|
||||
async Task<bool> PopulateApi(
|
||||
RepositoryResponse apiResponse,
|
||||
Components.Repository.IRepository repository,
|
||||
IRepository repository,
|
||||
IDatabaseContext databaseContext,
|
||||
Models.Instance instance,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -1008,7 +513,14 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
// rev info stuff
|
||||
Models.RevisionInformation revisionInfo = null;
|
||||
var needsDbUpdate = await LoadRevisionInformation(repository, databaseContext, instance, null, x => revisionInfo = x, cancellationToken);
|
||||
var needsDbUpdate = await RepositoryUpdateService.LoadRevisionInformation(
|
||||
repository,
|
||||
databaseContext,
|
||||
Logger,
|
||||
instance,
|
||||
null,
|
||||
newRevInfo => revisionInfo = newRevInfo,
|
||||
cancellationToken);
|
||||
apiResponse.RevisionInformation = revisionInfo.ToApi();
|
||||
return needsDbUpdate;
|
||||
}
|
||||
|
||||
@@ -801,7 +801,7 @@ namespace Tgstation.Server.Host.Setup
|
||||
{
|
||||
var config = new ControlPanelConfiguration
|
||||
{
|
||||
Enable = await PromptYesNo("Enable the web control panel (Incomplete)? (y/n): ", cancellationToken),
|
||||
Enable = await PromptYesNo("Enable the web control panel? (y/n): ", cancellationToken),
|
||||
AllowAnyOrigin = await PromptYesNo("Allow web control panels hosted elsewhere to access the server? (Access-Control-Allow-Origin: *) (y/n): ", cancellationToken),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user