tgstation-server 6.19.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
RepositoryUpdateService.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Threading;
5using System.Threading.Tasks;
6
7using LibGit2Sharp;
8
9using Microsoft.EntityFrameworkCore;
10using Microsoft.Extensions.Logging;
11
17
19{
23 sealed class RepositoryUpdateService
24 {
28 readonly Models.RepositorySettings currentModel;
29
33 readonly User initiatingUser;
34
38 readonly ILogger<RepositoryUpdateService> logger;
39
43 readonly long instanceId;
44
52 public RepositoryUpdateService(
53 Models.RepositorySettings currentModel,
54 User initiatingUser,
55 ILogger<RepositoryUpdateService> logger,
56 long instanceId)
57 {
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;
62 }
63
75 public static async ValueTask<bool> LoadRevisionInformation(
76 IRepository repository,
77 IDatabaseContext databaseContext,
78 ILogger logger,
79 Models.Instance instance,
80 string? lastOriginCommitSha,
81 Action<Models.RevisionInformation>? revInfoSink,
82 CancellationToken cancellationToken)
83 {
84 var repoSha = repository.Head;
85
86 IQueryable<Models.RevisionInformation> ApplyQuery(IQueryable<Models.RevisionInformation> query) => query
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);
92
93 var revisionInfo = await ApplyQuery(
94 databaseContext
95 .RevisionInformations)
96 .FirstOrDefaultAsync(cancellationToken);
97
98 // If the DB doesn't have it, check the local set
99 if (revisionInfo == default)
100 revisionInfo = databaseContext
102 .Local
103 .Where(x => x.CommitSha == repoSha && x.InstanceId == instance.Id)
104 .FirstOrDefault();
105
106 var needsDbUpdate = revisionInfo == default;
107 if (needsDbUpdate)
108 {
109 // needs insertion
110 revisionInfo = new Models.RevisionInformation
111 {
112 Instance = instance,
113 CommitSha = repoSha,
114 Timestamp = await repository.TimestampCommit(repoSha, cancellationToken),
115 CompileJobs = new List<CompileJob>(),
116 ActiveTestMerges = new List<RevInfoTestMerge>(), // non null vals for api returns
117 };
118
119 lock (databaseContext) // cleaner this way
120 databaseContext.RevisionInformations.Add(revisionInfo);
121 }
122
123 revisionInfo!.OriginCommitSha ??= lastOriginCommitSha;
124 if (revisionInfo.OriginCommitSha == null)
125 {
126 revisionInfo.OriginCommitSha = repoSha;
127 logger.LogInformation(Repository.OriginTrackingErrorTemplate, repoSha);
128 }
129
130 revInfoSink?.Invoke(revisionInfo);
131 return needsDbUpdate;
132 }
133
143#pragma warning disable CA1502, CA1506 // TODO: Decomplexify
144 public async ValueTask RepositoryUpdateJob(
146 IInstanceCore? instance,
147 IDatabaseContextFactory databaseContextFactory,
148 JobProgressReporter progressReporter,
149 CancellationToken cancellationToken)
150#pragma warning restore CA1502, CA1506
151 {
152 ArgumentNullException.ThrowIfNull(model);
153 ArgumentNullException.ThrowIfNull(instance);
154 ArgumentNullException.ThrowIfNull(databaseContextFactory);
155 ArgumentNullException.ThrowIfNull(progressReporter);
156
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;
160
161 var startReference = repo.Reference;
162 var startSha = repo.Head;
163 string? postUpdateSha = null;
164
165 var newTestMerges = model.NewTestMerges != null && model.NewTestMerges.Count > 0;
166
167 if (newTestMerges && repo.RemoteGitProvider == RemoteGitProvider.Unknown)
168 throw new JobException(ErrorCode.RepoUnsupportedTestMergeRemote);
169
170 var committerName = (currentModel.ShowTestMergeCommitters!.Value
171 ? initiatingUser.Name
172 : currentModel.CommitterName)!;
173
174 var hardResettingToOriginReference = model.UpdateFromOrigin == true && model.Reference != null;
175
176 var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1));
177 var progressFactor = 1.0 / numSteps;
178
179 JobProgressReporter NextProgressReporter(string? stage) => progressReporter.CreateSection(stage, progressFactor);
180
181 progressReporter.ReportProgress(0);
182
183 // get a base line for where we are
184 Models.RevisionInformation? lastRevisionInfo = null;
185
186 var attachedInstance = new Models.Instance
187 {
188 Id = instanceId,
189 };
190
191 ValueTask CallLoadRevInfo(Models.TestMerge? testMergeToAdd = null, string? lastOriginCommitSha = null) => databaseContextFactory
192 .UseContext(
193 async databaseContext =>
194 {
195 databaseContext.Instances.Attach(attachedInstance);
196 var previousRevInfo = lastRevisionInfo!;
197 var needsUpdate = await LoadRevisionInformation(
198 repo,
199 databaseContext,
200 logger,
201 attachedInstance,
202 lastOriginCommitSha,
203 x => lastRevisionInfo = x,
204 cancellationToken);
205
206 if (testMergeToAdd != null)
207 {
208 // rev info may have already loaded the user
209 var mergedBy = databaseContext.Users.Local.FirstOrDefault(x => x.Id == initiatingUser.Id);
210 if (mergedBy == default)
211 {
212 mergedBy = new User
213 {
214 Id = initiatingUser.Id,
215 };
216
217 databaseContext.Users.Attach(mergedBy);
218 }
219
220 testMergeToAdd.MergedBy = mergedBy;
221 testMergeToAdd.MergedAt = DateTimeOffset.UtcNow;
222
223 var activeTestMerges = lastRevisionInfo!.ActiveTestMerges!;
224 foreach (var activeTestMerge in previousRevInfo.ActiveTestMerges!)
225 activeTestMerges.Add(activeTestMerge);
226
227 activeTestMerges.Add(new RevInfoTestMerge(testMergeToAdd, lastRevisionInfo));
228 lastRevisionInfo.PrimaryTestMerge = testMergeToAdd;
229
230 needsUpdate = true;
231 }
232
233 if (needsUpdate)
234 await databaseContext.Save(cancellationToken);
235 });
236
237 await CallLoadRevInfo();
238
239 // apply new rev info, tracking applied test merges
240 ValueTask UpdateRevInfo(Models.TestMerge? testMergeToAdd = null) => CallLoadRevInfo(testMergeToAdd, lastRevisionInfo!.OriginCommitSha);
241
242 try
243 {
244 // fetch/pull
245 if (model.UpdateFromOrigin == true)
246 {
247 if (!repo.Tracking)
248 throw new JobException(ErrorCode.RepoReferenceRequired);
249 using (var fetchReporter = NextProgressReporter("Fetch Origin"))
250 await repo.FetchOrigin(
251 fetchReporter,
252 currentModel.AccessUser,
253 currentModel.AccessToken,
254 false,
255 cancellationToken);
256
257 if (!modelHasShaOrReference)
258 {
259 bool? fastForward;
260 using (var mergeReporter = NextProgressReporter("Merge Origin"))
261 fastForward = await repo.MergeOrigin(
262 mergeReporter,
263 committerName,
264 currentModel.CommitterEmail!,
265 false,
266 cancellationToken);
267
268 if (!fastForward.HasValue)
269 throw new JobException(ErrorCode.RepoMergeConflict);
270
271 lastRevisionInfo!.OriginCommitSha = await repo.GetOriginSha(cancellationToken);
272 await UpdateRevInfo();
273 if (fastForward.Value)
274 {
275 using var syncReporter = NextProgressReporter("Sychronize");
276 await repo.Synchronize(
277 syncReporter,
278 currentModel.AccessUser,
279 currentModel.AccessToken,
280 currentModel.CommitterName!,
281 currentModel.CommitterEmail!,
282 true,
283 false,
284 cancellationToken);
285 postUpdateSha = repo.Head;
286 }
287 else
288 NextProgressReporter(null).Dispose();
289 }
290 }
291
292 var updateSubmodules = currentModel.UpdateSubmodules!.Value;
293
294 // checkout/hard reset
295 if (modelHasShaOrReference)
296 {
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);
303
304 if (validCheckoutSha || validCheckoutReference)
305 {
306 var committish = model.CheckoutSha ?? model.Reference!;
307 var isSha = await repo.IsSha(committish, cancellationToken);
308
309 if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null))
310 throw new JobException(ErrorCode.RepoSwappedShaOrReference);
311
312 using (var checkoutReporter = NextProgressReporter("Checkout"))
313 await repo.CheckoutObject(
314 committish,
315 currentModel.AccessUser,
316 currentModel.AccessToken,
317 updateSubmodules,
318 false,
319 checkoutReporter,
320 cancellationToken);
321 await CallLoadRevInfo(); // we've either seen origin before or what we're checking out is on origin
322 }
323 else
324 NextProgressReporter(null).Dispose();
325
326 if (hardResettingToOriginReference)
327 {
328 if (!repo.Tracking)
329 throw new JobException(ErrorCode.RepoReferenceNotTracking);
330 using (var resetReporter = NextProgressReporter("Reset to Origin"))
331 await repo.ResetToOrigin(
332 resetReporter,
333 currentModel.AccessUser,
334 currentModel.AccessToken,
335 updateSubmodules,
336 false,
337 cancellationToken);
338
339 using (var syncReporter = NextProgressReporter("Synchronize"))
340 await repo.Synchronize(
341 syncReporter,
342 currentModel.AccessUser,
343 currentModel.AccessToken,
344 currentModel.CommitterName!,
345 currentModel.CommitterEmail!,
346 true,
347 false,
348 cancellationToken);
349
350 await CallLoadRevInfo();
351
352 // repo head is on origin so force this
353 // will update the db if necessary
354 lastRevisionInfo!.OriginCommitSha = repo.Head;
355 }
356 }
357
358 // test merging
359 if (newTestMerges)
360 {
361 if (repo.RemoteGitProvider == RemoteGitProvider.Unknown)
362 throw new JobException(ErrorCode.RepoTestMergeInvalidRemote);
363
364 // bit of sanitization
365 var newTestMergeModels = model.NewTestMerges!;
366 foreach (var newTestMergeWithoutTargetCommitSha in newTestMergeModels.Where(x => String.IsNullOrWhiteSpace(x.TargetCommitSha)))
367 newTestMergeWithoutTargetCommitSha.TargetCommitSha = null;
368
369 var repoOwner = repo.RemoteRepositoryOwner;
370 var repoName = repo.RemoteRepositoryName;
371
372 // 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
373 Models.RevisionInformation? revInfoWereLookingFor = null;
374 bool needToApplyRemainingPrs = true;
375 if (lastRevisionInfo!.OriginCommitSha == lastRevisionInfo.CommitSha)
376 {
377 bool cantSearch = false;
378 foreach (var newTestMerge in newTestMergeModels)
379 {
380 if (newTestMerge.TargetCommitSha != null)
381#pragma warning disable CA1308 // Normalize strings to uppercase
382 newTestMerge.TargetCommitSha = newTestMerge.TargetCommitSha?.ToLowerInvariant(); // ala libgit2
383#pragma warning restore CA1308 // Normalize strings to uppercase
384 else
385 try
386 {
387 // retrieve the latest sha
388 var pr = await repo.GetTestMerge(newTestMerge, currentModel, cancellationToken);
389
390 // we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it
391 newTestMerge.TargetCommitSha = pr.TargetCommitSha;
392 }
393 catch
394 {
395 cantSearch = true;
396 break;
397 }
398 }
399
400 if (!cantSearch)
401 {
402 List<Models.RevisionInformation>? dbPull = null;
403
404 await databaseContextFactory.UseContext(
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));
415
416 // split here cause this bit has to be done locally
417 revInfoWereLookingFor = dbPull!
418 .Where(x => x.ActiveTestMerges!.Count == newTestMergeModels.Count
419 && x.ActiveTestMerges
420 .Select(y => y.TestMerge)
421 .All(y => newTestMergeModels
422 .Any(z =>
423 y.Number == z.Number
424 && y.TargetCommitSha!.StartsWith(z.TargetCommitSha!, StringComparison.Ordinal)
425 && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null))))
426 .FirstOrDefault();
427
428 if (revInfoWereLookingFor == default && newTestMergeModels.Count > 1)
429 {
430 // okay try to add at least SOME prs we've seen before
431 var listedNewTestMerges = newTestMergeModels.ToList();
432
433 var appliedTestMergeIds = new List<long>();
434
435 Models.RevisionInformation? lastGoodRevInfo = null;
436 do
437 {
438 foreach (var newTestMergeParameters in listedNewTestMerges)
439 {
440 revInfoWereLookingFor = dbPull!
441 .Where(testRevInfo =>
442 {
443 if (testRevInfo.PrimaryTestMerge == null)
444 return false;
445
446 var testMergeMatch = newTestMergeModels.Any(testTestMerge =>
447 {
448 var numberMatch = testRevInfo.PrimaryTestMerge.Number == testTestMerge.Number;
449 if (!numberMatch)
450 return false;
451
452 var shaMatch = testRevInfo.PrimaryTestMerge.TargetCommitSha!.StartsWith(
453 testTestMerge.TargetCommitSha!,
454 StringComparison.Ordinal);
455 if (!shaMatch)
456 return false;
457
458 var commentMatch = testRevInfo.PrimaryTestMerge.Comment == testTestMerge.Comment;
459 return commentMatch;
460 });
461
462 if (!testMergeMatch)
463 return false;
464
465 var previousTestMergesMatch = testRevInfo
466 .ActiveTestMerges!
467 .Select(previousRevInfoTestMerge => previousRevInfoTestMerge.TestMerge)
468 .All(previousTestMerge => appliedTestMergeIds.Contains(previousTestMerge.Id));
469
470 return previousTestMergesMatch;
471 })
472 .FirstOrDefault();
473
474 if (revInfoWereLookingFor != null)
475 {
476 lastGoodRevInfo = revInfoWereLookingFor;
477 appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge!.Id);
478 listedNewTestMerges.Remove(newTestMergeParameters);
479 break;
480 }
481 }
482 }
483 while (revInfoWereLookingFor != null && listedNewTestMerges.Count > 0);
484
485 revInfoWereLookingFor = lastGoodRevInfo;
486 needToApplyRemainingPrs = listedNewTestMerges.Count != 0;
487 if (needToApplyRemainingPrs)
488 model.NewTestMerges = listedNewTestMerges;
489 }
490 else if (revInfoWereLookingFor != null)
491 needToApplyRemainingPrs = false;
492 }
493 }
494
495 if (revInfoWereLookingFor != null)
496 {
497 // goteem
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;
503 }
504
505 if (needToApplyRemainingPrs)
506 {
507 foreach (var newTestMerge in newTestMergeModels)
508 {
509 if (lastRevisionInfo.ActiveTestMerges!.Any(x => x.TestMerge.Number == newTestMerge.Number))
510 throw new JobException(ErrorCode.RepoDuplicateTestMerge);
511
512 var fullTestMergeTask = repo.GetTestMerge(newTestMerge, currentModel, cancellationToken);
513
514 TestMergeResult mergeResult;
515 using (var testMergeReporter = NextProgressReporter($"Test merge #{newTestMerge.Number}"))
516 mergeResult = await repo.AddTestMerge(
517 newTestMerge,
518 committerName,
519 currentModel.CommitterEmail!,
520 currentModel.AccessUser,
521 currentModel.AccessToken,
522 updateSubmodules,
523 testMergeReporter,
524 cancellationToken);
525
526 if (mergeResult.Status == MergeStatus.Conflicts)
527 throw new JobException(
528 ErrorCode.RepoTestMergeConflict,
529 new JobException(
530 $"Test Merge #{newTestMerge.Number} at {newTestMerge.TargetCommitSha![..7]} conflicted! Conflicting files:{Environment.NewLine}{String.Join(Environment.NewLine, mergeResult.ConflictingFiles!.Select(file => $"\t- /{file}"))}"));
531
532 Models.TestMerge fullTestMerge;
533 try
534 {
535 fullTestMerge = await fullTestMergeTask;
536 }
537 catch (Exception ex)
538 {
539 logger.LogWarning("Error retrieving metadata for test merge #{testMergeNumber}!", newTestMerge.Number);
540
541 fullTestMerge = new Models.TestMerge
542 {
543 Author = ex.Message,
544 BodyAtMerge = ex.Message,
545 TitleAtMerge = ex.Message,
546 Comment = newTestMerge.Comment,
547 Number = newTestMerge.Number,
548 Url = ex.Message,
549 };
550 }
551
552 // Ensure we're getting the full sha from git itself
553 fullTestMerge.TargetCommitSha = newTestMerge.TargetCommitSha;
554
555 await UpdateRevInfo(fullTestMerge);
556 }
557 }
558 }
559
560 var currentHead = repo.Head;
561 if (currentModel.PushTestMergeCommits!.Value && (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead)))
562 {
563 using (var syncReporter = NextProgressReporter("Synchronize"))
564 await repo.Synchronize(
565 syncReporter,
566 currentModel.AccessUser,
567 currentModel.AccessToken,
568 currentModel.CommitterName!,
569 currentModel.CommitterEmail!,
570 false,
571 false,
572 cancellationToken);
573
574 await UpdateRevInfo();
575 }
576 }
577 catch
578 {
579 numSteps = 2;
580
581 // Forget what we've done and abort
582 progressReporter.ReportProgress(0.0);
583
584 var secondStep = startReference != null && repo.Head != startSha;
585
586 // DCTx2: Cancellation token is for job, operations should always run
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,
592 true,
593 false,
594 checkoutReporter,
595 default);
596
597 if (secondStep)
598 using (var resetReporter = progressReporter.CreateSection($"Hard reset to SHA {startSha[..7]}", 0.5))
599 await repo.ResetToSha(startSha, resetReporter, default);
600
601 throw;
602 }
603 }
604
613 public async ValueTask RepositoryRecloneJob(
614 IInstanceCore? instance,
615 IDatabaseContextFactory databaseContextFactory,
616 JobProgressReporter progressReporter,
617 CancellationToken cancellationToken)
618 {
619 ArgumentNullException.ThrowIfNull(instance);
620 ArgumentNullException.ThrowIfNull(databaseContextFactory);
621 ArgumentNullException.ThrowIfNull(progressReporter);
622
623 progressReporter.StageName = "Loading Old Repository";
624
625 Uri origin;
626 string? oldReference;
627 string oldSha;
628 ValueTask deleteTask;
629 using (var deleteReporter = progressReporter.CreateSection("Deleting Old Repository", 0.1))
630 {
631 using (var oldRepo = await instance.RepositoryManager.LoadRepository(cancellationToken))
632 {
633 if (oldRepo == null)
634 throw new JobException(ErrorCode.RepoMissing);
635
636 origin = oldRepo.Origin;
637 oldSha = oldRepo.Head;
638 oldReference = oldRepo.Reference;
639 if (oldReference == Repository.NoReference)
640 oldReference = null;
641
642 deleteTask = instance.RepositoryManager.DeleteRepository(cancellationToken);
643 }
644
645 await deleteTask;
646 }
647
648 try
649 {
650 using var cloneReporter = progressReporter.CreateSection("Cloning New Repository", 0.9);
651 using var newRepo = await instance.RepositoryManager.CloneRepository(
652 origin,
653 oldReference,
654 currentModel.AccessUser,
655 currentModel.AccessToken,
656 cloneReporter,
657 true, // TODO: Make configurable maybe...
658 cancellationToken)
659 ?? throw new JobException("A race condition occurred while recloning the repository. Somehow, it was fully cloned instantly after being deleted!"); // I'll take lines of code that should never be hit for $10k
660 }
661 catch (Exception ex) when (ex is not JobException)
662 {
663 logger.LogWarning("Reclone failed, clearing credentials!");
664
665 // need to clear credentials here
666 await databaseContextFactory.UseContextTaskReturn(context =>
667 {
668 context.RepositorySettings.Attach(currentModel);
669 currentModel.AccessUser = null;
670 currentModel.AccessToken = null;
671 return context.Save(CancellationToken.None); // DCT: Must always run
672 });
673
674 throw;
675 }
676 }
677 }
678}
virtual ? long Id
The ID of the entity.
Definition EntityId.cs:14
Metadata about a server instance.
Definition Instance.cs:9
string? Reference
The branch or tag HEAD points to.
Represents configurable settings for a git 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.
Operation exceptions thrown from the context of a Models.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.
Definition ErrorCode.cs:12
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.
@ Id
Lookup the Api.Models.EntityId.Id of the Models.PermissionSet.