2using System.Collections.Generic;
5using System.Threading.Tasks;
9using Microsoft.EntityFrameworkCore;
10using Microsoft.Extensions.Logging;
23 sealed class RepositoryUpdateService
33 readonly
User initiatingUser;
38 readonly ILogger<RepositoryUpdateService> logger;
43 readonly
long instanceId;
52 public RepositoryUpdateService(
53 Models.RepositorySettings currentModel,
55 ILogger<RepositoryUpdateService> logger,
58 this.currentModel = currentModel ??
throw new ArgumentNullException(nameof(currentModel));
59 this.initiatingUser = initiatingUser ??
throw new ArgumentNullException(nameof(initiatingUser));
60 this.logger = logger ??
throw new ArgumentNullException(nameof(logger));
61 this.instanceId = instanceId;
75 public static async ValueTask<bool> LoadRevisionInformation(
76 IRepository repository,
79 Models.Instance instance,
80 string? lastOriginCommitSha,
81 Action<Models.RevisionInformation>? revInfoSink,
82 CancellationToken cancellationToken)
84 var repoSha = repository.Head;
87 .Where(x => x.CommitSha == repoSha && x.InstanceId == instance.Id)
88 .Include(x => x.CompileJobs)
89 .Include(x => x.ActiveTestMerges!)
90 .ThenInclude(x => x.TestMerge)
91 .ThenInclude(x => x.MergedBy);
93 var revisionInfo = await ApplyQuery(
95 .RevisionInformations)
96 .FirstOrDefaultAsync(cancellationToken);
99 if (revisionInfo ==
default)
100 revisionInfo = databaseContext
103 .Where(x => x.CommitSha == repoSha && x.InstanceId == instance.Id)
106 var needsDbUpdate = revisionInfo ==
default;
114 Timestamp = await repository.TimestampCommit(repoSha, cancellationToken),
116 ActiveTestMerges =
new List<RevInfoTestMerge>(),
119 lock (databaseContext)
123 revisionInfo!.OriginCommitSha ??= lastOriginCommitSha;
124 if (revisionInfo.OriginCommitSha ==
null)
126 revisionInfo.OriginCommitSha = repoSha;
127 logger.LogInformation(
Repository.OriginTrackingErrorTemplate, repoSha);
130 revInfoSink?.Invoke(revisionInfo);
131 return needsDbUpdate;
143#pragma warning disable CA1502, CA1506
144 public async ValueTask RepositoryUpdateJob(
146 IInstanceCore? instance,
149 CancellationToken cancellationToken)
150#pragma warning restore CA1502, CA1506
152 ArgumentNullException.ThrowIfNull(model);
153 ArgumentNullException.ThrowIfNull(instance);
154 ArgumentNullException.ThrowIfNull(databaseContextFactory);
155 ArgumentNullException.ThrowIfNull(progressReporter);
157 var repoManager = instance.RepositoryManager;
158 using var repo = await repoManager.LoadRepository(cancellationToken) ??
throw new JobException(
ErrorCode.RepoMissing);
159 var modelHasShaOrReference = model.CheckoutSha !=
null || model.Reference !=
null;
161 var startReference = repo.Reference;
162 var startSha = repo.Head;
163 string? postUpdateSha =
null;
165 var newTestMerges = model.NewTestMerges !=
null && model.NewTestMerges.Count > 0;
170 var committerName = (currentModel.ShowTestMergeCommitters!.Value
171 ? initiatingUser.Name
172 : currentModel.CommitterName)!;
174 var hardResettingToOriginReference = model.UpdateFromOrigin ==
true && model.Reference !=
null;
176 var numSteps = (model.
NewTestMerges?.Count ?? 0) + (model.
UpdateFromOrigin ==
true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1));
177 var progressFactor = 1.0 / numSteps;
184 Models.RevisionInformation? lastRevisionInfo =
null;
191 ValueTask CallLoadRevInfo(Models.TestMerge? testMergeToAdd =
null,
string? lastOriginCommitSha =
null) => databaseContextFactory
193 async databaseContext =>
196 var previousRevInfo = lastRevisionInfo!;
197 var needsUpdate = await LoadRevisionInformation(
203 x => lastRevisionInfo = x,
206 if (testMergeToAdd !=
null)
209 var mergedBy = databaseContext.
Users.
Local.FirstOrDefault(x => x.Id == initiatingUser.
Id);
210 if (mergedBy ==
default)
214 Id = initiatingUser.
Id,
220 testMergeToAdd.MergedBy = mergedBy;
221 testMergeToAdd.MergedAt = DateTimeOffset.UtcNow;
223 var activeTestMerges = lastRevisionInfo!.ActiveTestMerges!;
224 foreach (var activeTestMerge
in previousRevInfo.ActiveTestMerges!)
225 activeTestMerges.Add(activeTestMerge);
227 activeTestMerges.Add(
new RevInfoTestMerge(testMergeToAdd, lastRevisionInfo));
228 lastRevisionInfo.PrimaryTestMerge = testMergeToAdd;
234 await databaseContext.
Save(cancellationToken);
237 await CallLoadRevInfo();
240 ValueTask UpdateRevInfo(Models.TestMerge? testMergeToAdd =
null) => CallLoadRevInfo(testMergeToAdd, lastRevisionInfo!.OriginCommitSha);
249 using (var fetchReporter = NextProgressReporter(
"Fetch Origin"))
250 await repo.FetchOrigin(
252 currentModel.AccessUser,
253 currentModel.AccessToken,
257 if (!modelHasShaOrReference)
260 using (var mergeReporter = NextProgressReporter(
"Merge Origin"))
261 fastForward = await repo.MergeOrigin(
264 currentModel.CommitterEmail!,
268 if (!fastForward.HasValue)
271 lastRevisionInfo!.OriginCommitSha = await repo.GetOriginSha(cancellationToken);
272 await UpdateRevInfo();
273 if (fastForward.Value)
275 using var syncReporter = NextProgressReporter(
"Sychronize");
276 await repo.Synchronize(
278 currentModel.AccessUser,
279 currentModel.AccessToken,
280 currentModel.CommitterName!,
281 currentModel.CommitterEmail!,
285 postUpdateSha = repo.Head;
288 NextProgressReporter(
null).Dispose();
292 var updateSubmodules = currentModel.UpdateSubmodules!.Value;
295 if (modelHasShaOrReference)
297 var validCheckoutSha =
298 model.CheckoutSha !=
null
299 && !repo.Head.StartsWith(model.
CheckoutSha, StringComparison.OrdinalIgnoreCase);
300 var validCheckoutReference =
301 model.Reference !=
null
302 && !repo.Reference.Equals(model.
Reference, StringComparison.OrdinalIgnoreCase);
304 if (validCheckoutSha || validCheckoutReference)
306 var committish = model.CheckoutSha ?? model.
Reference!;
307 var isSha = await repo.IsSha(committish, cancellationToken);
312 using (var checkoutReporter = NextProgressReporter(
"Checkout"))
313 await repo.CheckoutObject(
315 currentModel.AccessUser,
316 currentModel.AccessToken,
321 await CallLoadRevInfo();
324 NextProgressReporter(
null).Dispose();
326 if (hardResettingToOriginReference)
330 using (var resetReporter = NextProgressReporter(
"Reset to Origin"))
331 await repo.ResetToOrigin(
333 currentModel.AccessUser,
334 currentModel.AccessToken,
339 using (var syncReporter = NextProgressReporter(
"Synchronize"))
340 await repo.Synchronize(
342 currentModel.AccessUser,
343 currentModel.AccessToken,
344 currentModel.CommitterName!,
345 currentModel.CommitterEmail!,
350 await CallLoadRevInfo();
354 lastRevisionInfo!.OriginCommitSha = repo.Head;
366 foreach (var newTestMergeWithoutTargetCommitSha
in newTestMergeModels.Where(x => String.IsNullOrWhiteSpace(x.TargetCommitSha)))
367 newTestMergeWithoutTargetCommitSha.TargetCommitSha =
null;
369 var repoOwner = repo.RemoteRepositoryOwner;
370 var repoName = repo.RemoteRepositoryName;
373 Models.RevisionInformation? revInfoWereLookingFor =
null;
374 bool needToApplyRemainingPrs =
true;
375 if (lastRevisionInfo!.OriginCommitSha == lastRevisionInfo.CommitSha)
377 bool cantSearch =
false;
378 foreach (var newTestMerge
in newTestMergeModels)
380 if (newTestMerge.TargetCommitSha !=
null)
381#pragma warning disable CA1308
382 newTestMerge.TargetCommitSha = newTestMerge.TargetCommitSha?.ToLowerInvariant();
383#pragma warning restore CA1308
388 var pr = await repo.GetTestMerge(newTestMerge, currentModel, cancellationToken);
391 newTestMerge.TargetCommitSha = pr.TargetCommitSha;
402 List<Models.RevisionInformation>? dbPull =
null;
405 async databaseContext =>
406 dbPull = await databaseContext
407 .RevisionInformations
408 .Where(x => x.InstanceId == instanceId
409 && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha
410 && x.ActiveTestMerges!.Count <= newTestMergeModels.Count
411 && x.ActiveTestMerges!.Count > 0)
412 .Include(x => x.ActiveTestMerges!)
413 .ThenInclude(x => x.TestMerge)
414 .ToListAsync(cancellationToken));
417 revInfoWereLookingFor = dbPull!
418 .Where(x => x.ActiveTestMerges!.Count == newTestMergeModels.Count
419 && x.ActiveTestMerges
420 .Select(y => y.TestMerge)
421 .All(y => newTestMergeModels
424 && y.TargetCommitSha!.StartsWith(z.TargetCommitSha!, StringComparison.Ordinal)
425 && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment ==
null))))
428 if (revInfoWereLookingFor ==
default && newTestMergeModels.Count > 1)
431 var listedNewTestMerges = newTestMergeModels.ToList();
433 var appliedTestMergeIds =
new List<long>();
435 Models.RevisionInformation? lastGoodRevInfo =
null;
438 foreach (var newTestMergeParameters
in listedNewTestMerges)
440 revInfoWereLookingFor = dbPull!
441 .Where(testRevInfo =>
443 if (testRevInfo.PrimaryTestMerge ==
null)
446 var testMergeMatch = newTestMergeModels.Any(testTestMerge =>
448 var numberMatch = testRevInfo.PrimaryTestMerge.Number == testTestMerge.Number;
452 var shaMatch = testRevInfo.PrimaryTestMerge.TargetCommitSha!.StartsWith(
453 testTestMerge.TargetCommitSha!,
454 StringComparison.Ordinal);
458 var commentMatch = testRevInfo.PrimaryTestMerge.Comment == testTestMerge.Comment;
465 var previousTestMergesMatch = testRevInfo
467 .Select(previousRevInfoTestMerge => previousRevInfoTestMerge.TestMerge)
468 .All(previousTestMerge => appliedTestMergeIds.Contains(previousTestMerge.Id));
470 return previousTestMergesMatch;
474 if (revInfoWereLookingFor !=
null)
476 lastGoodRevInfo = revInfoWereLookingFor;
477 appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge!.Id);
478 listedNewTestMerges.Remove(newTestMergeParameters);
483 while (revInfoWereLookingFor !=
null && listedNewTestMerges.Count > 0);
485 revInfoWereLookingFor = lastGoodRevInfo;
486 needToApplyRemainingPrs = listedNewTestMerges.Count != 0;
487 if (needToApplyRemainingPrs)
488 model.NewTestMerges = listedNewTestMerges;
490 else if (revInfoWereLookingFor !=
null)
491 needToApplyRemainingPrs =
false;
495 if (revInfoWereLookingFor !=
null)
498 var commitSha = revInfoWereLookingFor.CommitSha!;
499 logger.LogDebug(
"Reusing existing SHA {sha}...", commitSha);
500 using var resetReporter = NextProgressReporter($
"Reset to {commitSha[..7]}");
501 await repo.ResetToSha(commitSha, resetReporter, cancellationToken);
502 lastRevisionInfo = revInfoWereLookingFor;
505 if (needToApplyRemainingPrs)
507 foreach (var newTestMerge
in newTestMergeModels)
509 if (lastRevisionInfo.ActiveTestMerges!.Any(x => x.TestMerge.Number == newTestMerge.Number))
512 var fullTestMergeTask = repo.GetTestMerge(newTestMerge, currentModel, cancellationToken);
514 TestMergeResult mergeResult;
515 using (var testMergeReporter = NextProgressReporter($
"Test merge #{newTestMerge.Number}"))
516 mergeResult = await repo.AddTestMerge(
519 currentModel.CommitterEmail!,
520 currentModel.AccessUser,
521 currentModel.AccessToken,
526 if (mergeResult.Status == MergeStatus.Conflicts)
530 $
"Test Merge #{newTestMerge.Number} at {newTestMerge.TargetCommitSha![..7]} conflicted! Conflicting files:{Environment.NewLine}{String.Join(Environment.NewLine, mergeResult.ConflictingFiles!.Select(file => $"\t- /{file}
"))}"));
535 fullTestMerge = await fullTestMergeTask;
539 logger.LogWarning(
"Error retrieving metadata for test merge #{testMergeNumber}!", newTestMerge.Number);
544 BodyAtMerge = ex.Message,
545 TitleAtMerge = ex.Message,
546 Comment = newTestMerge.Comment,
547 Number = newTestMerge.Number,
553 fullTestMerge.TargetCommitSha = newTestMerge.TargetCommitSha;
555 await UpdateRevInfo(fullTestMerge);
560 var currentHead = repo.Head;
561 if (currentModel.PushTestMergeCommits!.Value && (startSha != currentHead || (postUpdateSha !=
null && postUpdateSha != currentHead)))
563 using (var syncReporter = NextProgressReporter(
"Synchronize"))
564 await repo.Synchronize(
566 currentModel.AccessUser,
567 currentModel.AccessToken,
568 currentModel.CommitterName!,
569 currentModel.CommitterEmail!,
574 await UpdateRevInfo();
584 var secondStep = startReference !=
null && repo.Head != startSha;
587 using (var checkoutReporter = progressReporter.
CreateSection($
"Checkout {startReference ?? startSha[..7]}", secondStep ? 0.5 : 1.0))
588 await repo.CheckoutObject(
589 startReference ?? startSha,
590 currentModel.AccessUser,
591 currentModel.AccessToken,
598 using (var resetReporter = progressReporter.
CreateSection($
"Hard reset to SHA {startSha[..7]}", 0.5))
599 await repo.ResetToSha(startSha, resetReporter,
default);
613 public async ValueTask RepositoryRecloneJob(
614 IInstanceCore? instance,
617 CancellationToken cancellationToken)
619 ArgumentNullException.ThrowIfNull(instance);
620 ArgumentNullException.ThrowIfNull(databaseContextFactory);
621 ArgumentNullException.ThrowIfNull(progressReporter);
623 progressReporter.StageName =
"Loading Old Repository";
626 string? oldReference;
628 ValueTask deleteTask;
629 using (var deleteReporter = progressReporter.
CreateSection(
"Deleting Old Repository", 0.1))
631 using (var oldRepo = await instance.RepositoryManager.LoadRepository(cancellationToken))
636 origin = oldRepo.Origin;
637 oldSha = oldRepo.Head;
638 oldReference = oldRepo.Reference;
642 deleteTask = instance.RepositoryManager.DeleteRepository(cancellationToken);
650 using var cloneReporter = progressReporter.
CreateSection(
"Cloning New Repository", 0.9);
651 using var newRepo = await instance.RepositoryManager.CloneRepository(
654 currentModel.AccessUser,
655 currentModel.AccessToken,
659 ??
throw new JobException(
"A race condition occurred while recloning the repository. Somehow, it was fully cloned instantly after being deleted!");
663 logger.LogWarning(
"Reclone failed, clearing credentials!");
668 context.RepositorySettings.Attach(currentModel);
669 currentModel.AccessUser =
null;
670 currentModel.AccessToken =
null;
671 return context.Save(CancellationToken.None);
virtual ? long Id
The ID of the entity.
Metadata about a server instance.
string? Reference
The branch or tag HEAD points to.
Represents configurable settings for a git repository.
Represents a request to change the repository.
bool? UpdateFromOrigin
Do the equivalent of a git pull. Will attempt to merge unless RepositoryApiBase.Reference is also spe...
ICollection< TestMergeParameters >? NewTestMerges
TestMergeParameters for new TestMerges. Note that merges that conflict will not be performed.
string? CheckoutSha
The commit HEAD should point to.
Operation exceptions thrown from the context of a Models.Job.
Progress reporter for a Job.
JobProgressReporter CreateSection(string? newStageName, double percentage)
Create a subsection of the JobProgressReporter with its optional own stage name.
void ReportProgress(double? progress)
Report progress.
Many to many relationship for Models.RevisionInformation and Models.TestMerge.
void Attach(TModel model)
Attach a given model to the the working set.
IEnumerable< TModel > Local
An IEnumerable<T> of TModel s prioritizing in the working set.
void Add(TModel model)
Add a given model to the the working set.
Factory for scoping usage of IDatabaseContexts. Meant for use by Components.
ValueTask UseContextTaskReturn(Func< IDatabaseContext, Task > operation)
Run an operation in the scope of an IDatabaseContext.
ValueTask UseContext(Func< IDatabaseContext, ValueTask > operation)
Run an operation in the scope of an IDatabaseContext.
IDatabaseCollection< RevisionInformation > RevisionInformations
The RevisionInformations in the IDatabaseContext.
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext.
IDatabaseCollection< User > Users
The Users in the IDatabaseContext.
IDatabaseCollection< Instance > Instances
The Instances in the IDatabaseContext.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
RemoteGitProvider
Indicates the remote git host.
@ List
User may list files if the Models.Instance allows it.
@ CompileJobs
User may list and read all Models.Internal.CompileJobs.
@ Repository
RepositoryRights.
@ Id
Lookup the Api.Models.EntityId.Id of the Models.PermissionSet.