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