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(databaseContext.
RevisionInformations).FirstOrDefaultAsync(cancellationToken);
96 if (revisionInfo ==
default)
97 revisionInfo = databaseContext
100 .Where(x => x.CommitSha == repoSha && x.InstanceId == instance.Id)
103 var needsDbUpdate = revisionInfo ==
default;
111 Timestamp = await repository.TimestampCommit(repoSha, cancellationToken),
113 ActiveTestMerges =
new List<RevInfoTestMerge>(),
116 lock (databaseContext)
120 revisionInfo!.OriginCommitSha ??= lastOriginCommitSha;
121 if (revisionInfo.OriginCommitSha ==
null)
123 revisionInfo.OriginCommitSha = repoSha;
124 logger.LogInformation(
Repository.OriginTrackingErrorTemplate, repoSha);
127 revInfoSink?.Invoke(revisionInfo);
128 return needsDbUpdate;
140#pragma warning disable CA1502, CA1506
141 public async ValueTask RepositoryUpdateJob(
143 IInstanceCore? instance,
146 CancellationToken cancellationToken)
147#pragma warning restore CA1502, CA1506
149 ArgumentNullException.ThrowIfNull(model);
150 ArgumentNullException.ThrowIfNull(instance);
151 ArgumentNullException.ThrowIfNull(databaseContextFactory);
152 ArgumentNullException.ThrowIfNull(progressReporter);
154 var repoManager = instance.RepositoryManager;
155 using var repo = await repoManager.LoadRepository(cancellationToken) ??
throw new JobException(
ErrorCode.RepoMissing);
156 var modelHasShaOrReference = model.CheckoutSha !=
null || model.Reference !=
null;
158 var startReference = repo.Reference;
159 var startSha = repo.Head;
160 string? postUpdateSha =
null;
162 var newTestMerges = model.NewTestMerges !=
null && model.NewTestMerges.Count > 0;
167 var committerName = (currentModel.ShowTestMergeCommitters!.Value
168 ? initiatingUser.Name
169 : currentModel.CommitterName)!;
171 var hardResettingToOriginReference = model.UpdateFromOrigin ==
true && model.Reference !=
null;
173 var numSteps = (model.
NewTestMerges?.Count ?? 0) + (model.
UpdateFromOrigin ==
true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1));
174 var progressFactor = 1.0 / numSteps;
181 Models.RevisionInformation? lastRevisionInfo =
null;
188 ValueTask CallLoadRevInfo(Models.TestMerge? testMergeToAdd =
null,
string? lastOriginCommitSha =
null) => databaseContextFactory
190 async databaseContext =>
193 var previousRevInfo = lastRevisionInfo!;
194 var needsUpdate = await LoadRevisionInformation(
200 x => lastRevisionInfo = x,
203 if (testMergeToAdd !=
null)
206 var mergedBy = databaseContext.
Users.
Local.FirstOrDefault(x => x.Id == initiatingUser.
Id);
207 if (mergedBy ==
default)
211 Id = initiatingUser.
Id,
217 testMergeToAdd.MergedBy = mergedBy;
218 testMergeToAdd.MergedAt = DateTimeOffset.UtcNow;
220 var activeTestMerges = lastRevisionInfo!.ActiveTestMerges!;
221 foreach (var activeTestMerge
in previousRevInfo.ActiveTestMerges!)
222 activeTestMerges.Add(activeTestMerge);
224 activeTestMerges.Add(
new RevInfoTestMerge(testMergeToAdd, lastRevisionInfo));
225 lastRevisionInfo.PrimaryTestMerge = testMergeToAdd;
231 await databaseContext.
Save(cancellationToken);
234 await CallLoadRevInfo();
237 ValueTask UpdateRevInfo(Models.TestMerge? testMergeToAdd =
null) => CallLoadRevInfo(testMergeToAdd, lastRevisionInfo!.OriginCommitSha);
246 using (var fetchReporter = NextProgressReporter(
"Fetch Origin"))
247 await repo.FetchOrigin(
249 currentModel.AccessUser,
250 currentModel.AccessToken,
254 if (!modelHasShaOrReference)
257 using (var mergeReporter = NextProgressReporter(
"Merge Origin"))
258 fastForward = await repo.MergeOrigin(
261 currentModel.CommitterEmail!,
265 if (!fastForward.HasValue)
268 lastRevisionInfo!.OriginCommitSha = await repo.GetOriginSha(cancellationToken);
269 await UpdateRevInfo();
270 if (fastForward.Value)
272 using var syncReporter = NextProgressReporter(
"Sychronize");
273 await repo.Synchronize(
275 currentModel.AccessUser,
276 currentModel.AccessToken,
277 currentModel.CommitterName!,
278 currentModel.CommitterEmail!,
282 postUpdateSha = repo.Head;
285 NextProgressReporter(
null).Dispose();
289 var updateSubmodules = currentModel.UpdateSubmodules!.Value;
292 if (modelHasShaOrReference)
294 var validCheckoutSha =
295 model.CheckoutSha !=
null
296 && !repo.Head.StartsWith(model.
CheckoutSha, StringComparison.OrdinalIgnoreCase);
297 var validCheckoutReference =
298 model.Reference !=
null
299 && !repo.Reference.Equals(model.
Reference, StringComparison.OrdinalIgnoreCase);
301 if (validCheckoutSha || validCheckoutReference)
303 var committish = model.CheckoutSha ?? model.
Reference!;
304 var isSha = await repo.IsSha(committish, cancellationToken);
309 using (var checkoutReporter = NextProgressReporter(
"Checkout"))
310 await repo.CheckoutObject(
312 currentModel.AccessUser,
313 currentModel.AccessToken,
318 await CallLoadRevInfo();
321 NextProgressReporter(
null).Dispose();
323 if (hardResettingToOriginReference)
327 using (var resetReporter = NextProgressReporter(
"Reset to Origin"))
328 await repo.ResetToOrigin(
330 currentModel.AccessUser,
331 currentModel.AccessToken,
336 using (var syncReporter = NextProgressReporter(
"Synchronize"))
337 await repo.Synchronize(
339 currentModel.AccessUser,
340 currentModel.AccessToken,
341 currentModel.CommitterName!,
342 currentModel.CommitterEmail!,
347 await CallLoadRevInfo();
351 lastRevisionInfo!.OriginCommitSha = repo.Head;
363 foreach (var newTestMergeWithoutTargetCommitSha
in newTestMergeModels.Where(x => String.IsNullOrWhiteSpace(x.TargetCommitSha)))
364 newTestMergeWithoutTargetCommitSha.TargetCommitSha =
null;
366 var repoOwner = repo.RemoteRepositoryOwner;
367 var repoName = repo.RemoteRepositoryName;
370 Models.RevisionInformation? revInfoWereLookingFor =
null;
371 bool needToApplyRemainingPrs =
true;
372 if (lastRevisionInfo!.OriginCommitSha == lastRevisionInfo.CommitSha)
374 bool cantSearch =
false;
375 foreach (var newTestMerge
in newTestMergeModels)
377 if (newTestMerge.TargetCommitSha !=
null)
378#pragma warning disable CA1308
379 newTestMerge.TargetCommitSha = newTestMerge.TargetCommitSha?.ToLowerInvariant();
380#pragma warning restore CA1308
385 var pr = await repo.GetTestMerge(newTestMerge, currentModel, cancellationToken);
388 newTestMerge.TargetCommitSha = pr.TargetCommitSha;
399 List<Models.RevisionInformation>? dbPull =
null;
402 async databaseContext =>
405 .Where(x => x.InstanceId == instanceId
406 && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha
407 && x.ActiveTestMerges!.Count <= newTestMergeModels.Count
408 && x.ActiveTestMerges!.Count > 0)
409 .Include(x => x.ActiveTestMerges!)
410 .ThenInclude(x => x.TestMerge)
411 .ToListAsync(cancellationToken));
414 revInfoWereLookingFor = dbPull!
415 .Where(x => x.ActiveTestMerges!.Count == newTestMergeModels.Count
416 && x.ActiveTestMerges
417 .Select(y => y.TestMerge)
418 .All(y => newTestMergeModels
421 && y.TargetCommitSha!.StartsWith(z.TargetCommitSha!, StringComparison.Ordinal)
422 && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment ==
null))))
425 if (revInfoWereLookingFor ==
default && newTestMergeModels.Count > 1)
428 var listedNewTestMerges = newTestMergeModels.ToList();
430 var appliedTestMergeIds =
new List<long>();
432 Models.RevisionInformation? lastGoodRevInfo =
null;
435 foreach (var newTestMergeParameters
in listedNewTestMerges)
437 revInfoWereLookingFor = dbPull!
438 .Where(testRevInfo =>
440 if (testRevInfo.PrimaryTestMerge ==
null)
443 var testMergeMatch = newTestMergeModels.Any(testTestMerge =>
445 var numberMatch = testRevInfo.PrimaryTestMerge.Number == testTestMerge.Number;
449 var shaMatch = testRevInfo.PrimaryTestMerge.TargetCommitSha!.StartsWith(
450 testTestMerge.TargetCommitSha!,
451 StringComparison.Ordinal);
455 var commentMatch = testRevInfo.PrimaryTestMerge.Comment == testTestMerge.Comment;
462 var previousTestMergesMatch = testRevInfo
464 .Select(previousRevInfoTestMerge => previousRevInfoTestMerge.TestMerge)
465 .All(previousTestMerge => appliedTestMergeIds.Contains(previousTestMerge.Id));
467 return previousTestMergesMatch;
471 if (revInfoWereLookingFor !=
null)
473 lastGoodRevInfo = revInfoWereLookingFor;
474 appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge!.Id);
475 listedNewTestMerges.Remove(newTestMergeParameters);
480 while (revInfoWereLookingFor !=
null && listedNewTestMerges.Count > 0);
482 revInfoWereLookingFor = lastGoodRevInfo;
483 needToApplyRemainingPrs = listedNewTestMerges.Count != 0;
484 if (needToApplyRemainingPrs)
485 model.NewTestMerges = listedNewTestMerges;
487 else if (revInfoWereLookingFor !=
null)
488 needToApplyRemainingPrs =
false;
492 if (revInfoWereLookingFor !=
null)
495 var commitSha = revInfoWereLookingFor.CommitSha!;
496 logger.LogDebug(
"Reusing existing SHA {sha}...", commitSha);
497 using var resetReporter = NextProgressReporter($
"Reset to {commitSha[..7]}");
498 await repo.ResetToSha(commitSha, resetReporter, cancellationToken);
499 lastRevisionInfo = revInfoWereLookingFor;
502 if (needToApplyRemainingPrs)
504 foreach (var newTestMerge
in newTestMergeModels)
506 if (lastRevisionInfo.ActiveTestMerges!.Any(x => x.TestMerge.Number == newTestMerge.Number))
509 var fullTestMergeTask = repo.GetTestMerge(newTestMerge, currentModel, cancellationToken);
511 TestMergeResult mergeResult;
512 using (var testMergeReporter = NextProgressReporter($
"Test merge #{newTestMerge.Number}"))
513 mergeResult = await repo.AddTestMerge(
516 currentModel.CommitterEmail!,
517 currentModel.AccessUser,
518 currentModel.AccessToken,
523 if (mergeResult.Status == MergeStatus.Conflicts)
527 $
"Test Merge #{newTestMerge.Number} at {newTestMerge.TargetCommitSha![..7]} conflicted! Conflicting files:{Environment.NewLine}{String.Join(Environment.NewLine, mergeResult.ConflictingFiles!.Select(file => $"\t- /{file}
"))}"));
532 fullTestMerge = await fullTestMergeTask;
536 logger.LogWarning(
"Error retrieving metadata for test merge #{testMergeNumber}!", newTestMerge.Number);
541 BodyAtMerge = ex.Message,
542 TitleAtMerge = ex.Message,
543 Comment = newTestMerge.Comment,
544 Number = newTestMerge.Number,
550 fullTestMerge.TargetCommitSha = newTestMerge.TargetCommitSha;
552 await UpdateRevInfo(fullTestMerge);
557 var currentHead = repo.Head;
558 if (currentModel.PushTestMergeCommits!.Value && (startSha != currentHead || (postUpdateSha !=
null && postUpdateSha != currentHead)))
560 using (var syncReporter = NextProgressReporter(
"Synchronize"))
561 await repo.Synchronize(
563 currentModel.AccessUser,
564 currentModel.AccessToken,
565 currentModel.CommitterName!,
566 currentModel.CommitterEmail!,
571 await UpdateRevInfo();
581 var secondStep = startReference !=
null && repo.Head != startSha;
584 using (var checkoutReporter = progressReporter.
CreateSection($
"Checkout {startReference ?? startSha[..7]}", secondStep ? 0.5 : 1.0))
585 await repo.CheckoutObject(
586 startReference ?? startSha,
587 currentModel.AccessUser,
588 currentModel.AccessToken,
595 using (var resetReporter = progressReporter.
CreateSection($
"Hard reset to SHA {startSha[..7]}", 0.5))
596 await repo.ResetToSha(startSha, resetReporter,
default);
610 public async ValueTask RepositoryRecloneJob(
611 IInstanceCore? instance,
614 CancellationToken cancellationToken)
616 ArgumentNullException.ThrowIfNull(instance);
617 ArgumentNullException.ThrowIfNull(databaseContextFactory);
618 ArgumentNullException.ThrowIfNull(progressReporter);
620 progressReporter.StageName =
"Loading Old Repository";
623 string? oldReference;
625 ValueTask deleteTask;
626 using (var deleteReporter = progressReporter.
CreateSection(
"Deleting Old Repository", 0.1))
628 using (var oldRepo = await instance.RepositoryManager.LoadRepository(cancellationToken))
633 origin = oldRepo.Origin;
634 oldSha = oldRepo.Head;
635 oldReference = oldRepo.Reference;
639 deleteTask = instance.RepositoryManager.DeleteRepository(cancellationToken);
647 using var cloneReporter = progressReporter.
CreateSection(
"Cloning New Repository", 0.9);
648 using var newRepo = await instance.RepositoryManager.CloneRepository(
651 currentModel.AccessUser,
652 currentModel.AccessToken,
656 ??
throw new JobException(
"A race condition occurred while recloning the repository. Somehow, it was fully cloned instantly after being deleted!");
660 logger.LogWarning(
"Reclone failed, clearing credentials!");
665 context.RepositorySettings.Attach(currentModel);
666 currentModel.AccessUser =
null;
667 currentModel.AccessToken =
null;
668 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.