tgstation-server 6.19.1
The /tg/station 13 server suite
Loading...
Searching...
No Matches
Repository.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Globalization;
4using System.Linq;
5using System.Threading;
6using System.Threading.Tasks;
7
8using LibGit2Sharp;
9using LibGit2Sharp.Handlers;
10
11using Microsoft.Extensions.Logging;
12using Microsoft.Extensions.Options;
13
21
23{
25#pragma warning disable CA1506 // TODO: Decomplexify
27 {
31 public const string DefaultCommitterName = "tgstation-server";
32
36 public const string DefaultCommitterEmail = "tgstation-server@users.noreply.github.com";
37
41 public const string OriginTrackingErrorTemplate = "Unable to determine most recent origin commit of {sha}. Marking it as an origin commit. This may result in invalid git metadata until the next hard reset to an origin reference.";
42
46 public const string RemoteTemporaryBranchName = "___TGSTempBranch";
47
51 public const string NoReference = "(no branch)";
52
56 const string UnknownReference = "<UNKNOWN>";
57
60
63
66
68 public bool Tracking => Reference != null && libGitRepo.Head.IsTracking;
69
71 public string Head => libGitRepo.Head.Tip.Sha;
72
74 public string Reference => libGitRepo.Head.FriendlyName;
75
77 public Uri Origin => new(libGitRepo.Network.Remotes.First().Url);
78
82 readonly LibGit2Sharp.IRepository libGitRepo;
83
88
93
98
103
108
113
118
122 readonly IOptionsMonitor<GeneralConfiguration> generalConfigurationOptions;
123
127 readonly ILogger<Repository> logger;
128
136 static (PushOptions PushOptions, IEnumerable<JobProgressReporter> SubProgressReporters) GeneratePushOptions(JobProgressReporter progressReporter, CredentialsHandler credentialsHandler, CancellationToken cancellationToken)
137 {
138 var packFileCountingReporter = progressReporter.CreateSection(null, 0.25);
139 var packFileDeltafyingReporter = progressReporter.CreateSection(null, 0.25);
140 var transferProgressReporter = progressReporter.CreateSection(null, 0.5);
141
142 return (
144 {
145 OnPackBuilderProgress = (stage, current, total) =>
146 {
147 if (total < current)
148 total = current;
149
150 var percentage = ((double)current) / total;
151 (stage == PackBuilderStage.Counting ? packFileCountingReporter : packFileDeltafyingReporter).ReportProgress(percentage);
152 return !cancellationToken.IsCancellationRequested;
153 },
154 OnNegotiationCompletedBeforePush = (a) => !cancellationToken.IsCancellationRequested,
155 OnPushTransferProgress = (a, sentBytes, totalBytes) =>
156 {
157 packFileCountingReporter.ReportProgress((double)sentBytes / totalBytes);
158 return !cancellationToken.IsCancellationRequested;
159 },
160 CredentialsProvider = credentialsHandler,
161 },
162 SubProgressReporters: new List<JobProgressReporter>
163 {
164 packFileCountingReporter,
165 packFileDeltafyingReporter,
166 transferProgressReporter,
167 });
168 }
169
185 LibGit2Sharp.IRepository libGitRepo,
191 IGitRemoteFeaturesFactory gitRemoteFeaturesFactory,
193 IOptionsMonitor<GeneralConfiguration> generalConfigurationOptions,
194 ILogger<Repository> logger,
195 Action disposeAction)
196 : base(disposeAction)
197 {
198 this.libGitRepo = libGitRepo ?? throw new ArgumentNullException(nameof(libGitRepo));
199 this.commands = commands ?? throw new ArgumentNullException(nameof(commands));
200 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
201 this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
202 this.credentialsProvider = credentialsProvider ?? throw new ArgumentNullException(nameof(credentialsProvider));
203 this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
204 ArgumentNullException.ThrowIfNull(gitRemoteFeaturesFactory);
205 this.submoduleFactory = submoduleFactory ?? throw new ArgumentNullException(nameof(submoduleFactory));
206 this.generalConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
207 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
208
209 gitRemoteFeatures = gitRemoteFeaturesFactory.CreateGitRemoteFeatures(this);
210 }
211
213#pragma warning disable CA1506 // TODO: Decomplexify
214 public async ValueTask<TestMergeResult> AddTestMerge(
215 TestMergeParameters testMergeParameters,
216 string committerName,
217 string committerEmail,
218 string? username,
219 string? password,
220 bool updateSubmodules,
221 JobProgressReporter progressReporter,
222 CancellationToken cancellationToken)
223 {
224 ArgumentNullException.ThrowIfNull(testMergeParameters);
225 ArgumentNullException.ThrowIfNull(committerName);
226 ArgumentNullException.ThrowIfNull(committerEmail);
227 ArgumentNullException.ThrowIfNull(progressReporter);
228
229 logger.LogDebug(
230 "Begin AddTestMerge: #{prNumber} at {targetSha} ({comment}) by <{committerName} ({committerEmail})>",
231 testMergeParameters.Number,
232 testMergeParameters.TargetCommitSha?[..7],
233 testMergeParameters.Comment,
234 committerName,
235 committerEmail);
236
237 if (RemoteGitProvider == Api.Models.RemoteGitProvider.Unknown)
238 throw new InvalidOperationException("Cannot test merge with an Unknown RemoteGitProvider!");
239
240 var commitMessage = String.Format(
241 CultureInfo.InvariantCulture,
242 "TGS Test Merge (PR {0}){1}{2}",
243 testMergeParameters.Number,
244 testMergeParameters.Comment != null
245 ? Environment.NewLine
246 : String.Empty,
247 testMergeParameters.Comment ?? String.Empty);
248
249 var testMergeBranchName = String.Format(CultureInfo.InvariantCulture, "tm-{0}", testMergeParameters.Number);
250 var localBranchName = String.Format(CultureInfo.InvariantCulture, gitRemoteFeatures.TestMergeLocalBranchNameFormatter, testMergeParameters.Number, testMergeBranchName);
251
252 var refSpec = String.Format(CultureInfo.InvariantCulture, gitRemoteFeatures.TestMergeRefSpecFormatter, testMergeParameters.Number, testMergeBranchName);
253 var refSpecList = new List<string> { refSpec };
254 var logMessage = String.Format(CultureInfo.InvariantCulture, "Test merge #{0}", testMergeParameters.Number);
255
256 var originalCommit = libGitRepo.Head;
257
258 MergeResult? result = null;
259
260 var progressFactor = 1.0 / (updateSubmodules ? 3 : 2);
261
262 var sig = new Signature(new Identity(committerName, committerEmail), DateTimeOffset.UtcNow);
263 List<string>? conflictedPaths = null;
264
265 var credentialsHandler = await GenerateCredentialsHandler(username, password, cancellationToken);
266 await Task.Factory.StartNew(
267 () =>
268 {
269 try
270 {
271 try
272 {
273 logger.LogTrace("Fetching refspec {refSpec}...", refSpec);
274
275 var remote = libGitRepo.Network.Remotes.First();
276 using var fetchReporter = progressReporter.CreateSection($"Fetch {refSpec}", progressFactor);
279 refSpecList,
280 remote,
281 new FetchOptions().Hydrate(
282 logger,
283 fetchReporter,
284 credentialsHandler,
285 cancellationToken),
286 logMessage);
287 }
288 catch (UserCancelledException ex)
289 {
290 logger.LogTrace(ex, "Suppressing fetch cancel exception");
291 }
292 catch (LibGit2SharpException ex)
293 {
295 }
296
297 cancellationToken.ThrowIfCancellationRequested();
298
299 libGitRepo.RemoveUntrackedFiles();
300
301 cancellationToken.ThrowIfCancellationRequested();
302
303 var objectName = testMergeParameters.TargetCommitSha ?? localBranchName;
304 var gitObject = libGitRepo.Lookup(objectName) ?? throw new JobException($"Could not find object to merge: {objectName}");
305
306 testMergeParameters.TargetCommitSha = gitObject.Sha;
307
308 cancellationToken.ThrowIfCancellationRequested();
309
310 logger.LogTrace("Merging {targetCommitSha} into {currentReference}...", testMergeParameters.TargetCommitSha[..7], Reference);
311
312 using var mergeReporter = progressReporter.CreateSection($"Merge {testMergeParameters.TargetCommitSha[..7]}", progressFactor);
313 result = libGitRepo.Merge(testMergeParameters.TargetCommitSha, sig, new MergeOptions
314 {
315 CommitOnSuccess = commitMessage == null,
316 FailOnConflict = false, // Needed to get conflicting files
317 FastForwardStrategy = FastForwardStrategy.NoFastForward,
318 SkipReuc = true,
319 OnCheckoutProgress = CheckoutProgressHandler(mergeReporter),
320 });
321 }
322 finally
323 {
324 libGitRepo.Branches.Remove(localBranchName);
325 }
326
327 cancellationToken.ThrowIfCancellationRequested();
328
329 if (result.Status == MergeStatus.Conflicts)
330 {
331 var repoStatus = libGitRepo.RetrieveStatus();
332 conflictedPaths = new List<string>();
333 foreach (var file in repoStatus)
334 if (file.State == FileStatus.Conflicted)
335 conflictedPaths.Add(file.FilePath);
336
337 var revertTo = originalCommit.CanonicalName ?? originalCommit.Tip.Sha;
338 logger.LogDebug("Merge conflict, aborting and reverting to {revertTarget}", revertTo);
339 progressReporter.ReportProgress(0);
340 using var revertReporter = progressReporter.CreateSection("Hard Reset to {revertTo}", 1.0);
341 RawCheckout(revertTo, false, revertReporter, cancellationToken);
342 cancellationToken.ThrowIfCancellationRequested();
343 }
344
345 libGitRepo.RemoveUntrackedFiles();
346 },
347 cancellationToken,
349 TaskScheduler.Current);
350
351 if (result!.Status == MergeStatus.Conflicts)
352 {
353 var arguments = new List<string>
354 {
355 originalCommit.Tip.Sha,
356 testMergeParameters.TargetCommitSha!,
357 originalCommit.FriendlyName ?? UnknownReference,
358 testMergeBranchName,
359 };
360
361 arguments.AddRange(conflictedPaths!);
362
364 EventType.RepoMergeConflict,
365 arguments,
366 false,
367 false,
368 cancellationToken);
369 return new TestMergeResult
370 {
371 Status = result.Status,
372 ConflictingFiles = conflictedPaths,
373 };
374 }
375
376 if (result.Status != MergeStatus.UpToDate)
377 {
378 logger.LogTrace("Committing merge: \"{commitMessage}\"...", commitMessage);
379 await Task.Factory.StartNew(
380 () => libGitRepo.Commit(commitMessage, sig, sig, new CommitOptions
381 {
382 PrettifyMessage = true,
383 }),
384 cancellationToken,
386 TaskScheduler.Current);
387
388 if (updateSubmodules)
389 {
390 using var progressReporter2 = progressReporter.CreateSection("Update Submodules", progressFactor);
391 await UpdateSubmodules(
392 progressReporter2,
393 username,
394 password,
395 false,
396 cancellationToken);
397 }
398 }
399
401 EventType.RepoAddTestMerge,
402 new List<string?>
403 {
404 testMergeParameters.Number.ToString(CultureInfo.InvariantCulture),
405 testMergeParameters.TargetCommitSha!,
406 testMergeParameters.Comment,
407 },
408 false,
409 false,
410 cancellationToken);
411
412 return new TestMergeResult
413 {
414 Status = result.Status,
415 };
416 }
417#pragma warning restore CA1506
418
420 public async ValueTask CheckoutObject(
421 string committish,
422 string? username,
423 string? password,
424 bool updateSubmodules,
425 bool moveCurrentReference,
426 JobProgressReporter progressReporter,
427 CancellationToken cancellationToken)
428 {
429 ArgumentNullException.ThrowIfNull(committish);
430
431 logger.LogDebug("Checkout object: {committish}...", committish);
432 await eventConsumer.HandleEvent(EventType.RepoCheckout, new List<string> { committish, moveCurrentReference.ToString() }, false, false, cancellationToken);
433 await Task.Factory.StartNew(
434 () =>
435 {
436 libGitRepo.RemoveUntrackedFiles();
437 using var progressReporter3 = progressReporter.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0);
439 committish,
440 moveCurrentReference,
441 progressReporter3,
442 cancellationToken);
443 },
444 cancellationToken,
446 TaskScheduler.Current);
447
448 if (updateSubmodules)
449 {
450 using var progressReporter2 = progressReporter.CreateSection(null, 1.0 / 3);
451 await UpdateSubmodules(
452 progressReporter2,
453 username,
454 password,
455 false,
456 cancellationToken);
457 }
458 }
459
461 public async ValueTask FetchOrigin(
462 JobProgressReporter progressReporter,
463 string? username,
464 string? password,
465 bool deploymentPipeline,
466 CancellationToken cancellationToken)
467 {
468 logger.LogDebug("Fetch origin...");
469
470 var parameters = new List<string>();
471 var credentialsHandlerTask = GenerateCredentialsHandler(username, password, cancellationToken);
472 if (username != null)
473 {
474 parameters.Add(username);
475 if (password != null)
476 {
477 var transformedPassword = await gitRemoteFeatures.TransformRepositoryPassword(password, cancellationToken);
478 if (transformedPassword != null)
479 parameters.Add(transformedPassword);
480 }
481 }
482
483 await eventConsumer.HandleEvent(EventType.RepoFetch, parameters, true, deploymentPipeline, cancellationToken);
484 var credentialsHandler = await credentialsHandlerTask;
485 await Task.Factory.StartNew(
486 () =>
487 {
488 var remote = libGitRepo.Network.Remotes.First();
489 try
490 {
491 using var subReporter = progressReporter.CreateSection("Fetch Origin", 1.0);
492 var fetchOptions = new FetchOptions
493 {
494 Prune = true,
495 TagFetchMode = TagFetchMode.All,
496 }.Hydrate(
497 logger,
498 subReporter,
499 credentialsHandler,
500 cancellationToken);
501
504 remote
505 .FetchRefSpecs
506 .Select(x => x.Specification),
507 remote,
508 fetchOptions,
509 "Fetch origin commits");
510 }
511 catch (UserCancelledException)
512 {
513 cancellationToken.ThrowIfCancellationRequested();
514 }
515 catch (LibGit2SharpException ex)
516 {
518 }
519 },
520 cancellationToken,
522 TaskScheduler.Current);
523 }
524
526 public async ValueTask ResetToOrigin(
527 JobProgressReporter progressReporter,
528 string? username,
529 string? password,
530 bool updateSubmodules,
531 bool deploymentPipeline,
532 CancellationToken cancellationToken)
533 {
534 ArgumentNullException.ThrowIfNull(progressReporter);
535 if (!Tracking)
536 throw new JobException(ErrorCode.RepoReferenceRequired);
537 logger.LogTrace("Reset to origin...");
538 var trackedBranch = libGitRepo.Head.TrackedBranch;
539 await eventConsumer.HandleEvent(EventType.RepoResetOrigin, new List<string> { trackedBranch.FriendlyName, trackedBranch.Tip.Sha }, false, deploymentPipeline, cancellationToken);
540
541 using (var progressReporter2 = progressReporter.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0))
542 await ResetToSha(
543 trackedBranch.Tip.Sha,
544 progressReporter2,
545 cancellationToken);
546
547 if (updateSubmodules)
548 {
549 using var progressReporter3 = progressReporter.CreateSection(null, 1.0 / 3);
550 await UpdateSubmodules(
551 progressReporter3,
552 username,
553 password,
554 deploymentPipeline,
555 cancellationToken);
556 }
557 }
558
560 public Task ResetToSha(string sha, JobProgressReporter progressReporter, CancellationToken cancellationToken) => Task.Factory.StartNew(
561 () =>
562 {
563 ArgumentNullException.ThrowIfNull(sha);
564 ArgumentNullException.ThrowIfNull(progressReporter);
565
566 logger.LogDebug("Reset to sha: {sha}", sha[..7]);
567
568 libGitRepo.RemoveUntrackedFiles();
569 cancellationToken.ThrowIfCancellationRequested();
570
571 var gitObject = libGitRepo.Lookup(sha, ObjectType.Commit);
572 cancellationToken.ThrowIfCancellationRequested();
573
574 if (gitObject == null)
575 throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Cannot reset to non-existent SHA: {0}", sha));
576
577 libGitRepo.Reset(ResetMode.Hard, gitObject.Peel<Commit>(), new CheckoutOptions
578 {
579 OnCheckoutProgress = CheckoutProgressHandler(progressReporter.CreateSection($"Reset to {gitObject.Sha}", 1.0)),
580 });
581 },
582 cancellationToken,
584 TaskScheduler.Current);
585
587 public async ValueTask CopyTo(string path, CancellationToken cancellationToken)
588 {
589 ArgumentNullException.ThrowIfNull(path);
590 logger.LogTrace("Copying to {path}...", path);
592 new List<string> { ".git" },
593 (src, dest) =>
594 {
597
598 return ValueTask.CompletedTask;
599 },
601 path,
602 generalConfigurationOptions.CurrentValue.GetCopyDirectoryTaskThrottle(),
603 cancellationToken);
604 }
605
607 public Task<string> GetOriginSha(CancellationToken cancellationToken) => Task.Factory.StartNew(
608 () =>
609 {
610 if (!Tracking)
611 throw new JobException(ErrorCode.RepoReferenceRequired);
612
613 cancellationToken.ThrowIfCancellationRequested();
614
615 return libGitRepo.Head.TrackedBranch.Tip.Sha;
616 },
617 cancellationToken,
619 TaskScheduler.Current);
620
622 public async ValueTask<bool?> MergeOrigin(
623 JobProgressReporter progressReporter,
624 string committerName,
625 string committerEmail,
626 bool deploymentPipeline,
627 CancellationToken cancellationToken)
628 {
629 ArgumentNullException.ThrowIfNull(progressReporter);
630
631 MergeResult? result = null;
632 Branch? trackedBranch = null;
633
634 var oldHead = libGitRepo.Head;
635 var oldTip = oldHead.Tip;
636
637 await Task.Factory.StartNew(
638 () =>
639 {
640 if (!Tracking)
641 throw new JobException(ErrorCode.RepoReferenceRequired);
642
643 libGitRepo.RemoveUntrackedFiles();
644
645 cancellationToken.ThrowIfCancellationRequested();
646
647 trackedBranch = libGitRepo.Head.TrackedBranch;
648 logger.LogDebug(
649 "Merge origin/{trackedBranch}: <{committerName} ({committerEmail})>",
650 trackedBranch.FriendlyName,
651 committerName,
652 committerEmail);
653 result = libGitRepo.Merge(trackedBranch, new Signature(committerName, committerEmail, DateTimeOffset.UtcNow), new MergeOptions
654 {
655 CommitOnSuccess = true,
656 FailOnConflict = true,
657 FastForwardStrategy = FastForwardStrategy.Default,
658 SkipReuc = true,
659 OnCheckoutProgress = CheckoutProgressHandler(progressReporter.CreateSection("Merge Origin", 1.0)),
660 });
661
662 cancellationToken.ThrowIfCancellationRequested();
663
664 if (result.Status == MergeStatus.Conflicts)
665 {
666 logger.LogDebug("Merge conflict, aborting and reverting to {oldHeadFriendlyName}", oldHead.FriendlyName);
667 progressReporter.ReportProgress(0);
668 libGitRepo.Reset(ResetMode.Hard, oldTip, new CheckoutOptions
669 {
670 OnCheckoutProgress = CheckoutProgressHandler(progressReporter.CreateSection($"Hard Reset to {oldHead.FriendlyName}", 1.0)),
671 });
672 cancellationToken.ThrowIfCancellationRequested();
673 }
674
675 libGitRepo.RemoveUntrackedFiles();
676 },
677 cancellationToken,
679 TaskScheduler.Current);
680
681 if (result!.Status == MergeStatus.Conflicts)
682 {
684 EventType.RepoMergeConflict,
685 new List<string>
686 {
687 oldTip.Sha,
688 trackedBranch!.Tip.Sha,
689 oldHead.FriendlyName ?? UnknownReference,
690 trackedBranch.FriendlyName,
691 },
692 false,
693 deploymentPipeline,
694 cancellationToken);
695 return null;
696 }
697
698 return result.Status == MergeStatus.FastForward;
699 }
700
702 public async ValueTask<bool> Synchronize(
703 JobProgressReporter progressReporter,
704 string? username,
705 string? password,
706 string committerName,
707 string committerEmail,
708 bool synchronizeTrackedBranch,
709 bool deploymentPipeline,
710 CancellationToken cancellationToken)
711 {
712 ArgumentNullException.ThrowIfNull(committerName);
713 ArgumentNullException.ThrowIfNull(committerEmail);
714 ArgumentNullException.ThrowIfNull(progressReporter);
715
716 if (username == null && password == null)
717 {
718 logger.LogTrace("Not synchronizing due to lack of credentials!");
719 return false;
720 }
721
722 logger.LogTrace("Begin Synchronize...");
723
724 ArgumentNullException.ThrowIfNull(username);
725 ArgumentNullException.ThrowIfNull(password);
726
727 var startHead = Head;
728
729 logger.LogTrace("Configuring <{committerName} ({committerEmail})> as author/committer", committerName, committerEmail);
730 await Task.Factory.StartNew(
731 () =>
732 {
733 libGitRepo.Config.Set("user.name", committerName);
734 cancellationToken.ThrowIfCancellationRequested();
735 libGitRepo.Config.Set("user.email", committerEmail);
736 },
737 cancellationToken,
739 TaskScheduler.Current);
740
741 cancellationToken.ThrowIfCancellationRequested();
742 try
743 {
744 await eventConsumer.HandleEvent(
745 EventType.RepoPreSynchronize,
746 new List<string>
747 {
748 ioManager.ResolvePath(),
749 },
750 false,
751 deploymentPipeline,
752 cancellationToken);
753 }
754 finally
755 {
756 logger.LogTrace("Resetting and cleaning untracked files...");
757 await Task.Factory.StartNew(
758 () =>
759 {
760 using var resetProgress = progressReporter.CreateSection("Hard reset and remove untracked files", 0.1);
761 libGitRepo.Reset(ResetMode.Hard, libGitRepo.Head.Tip, new CheckoutOptions
762 {
763 OnCheckoutProgress = CheckoutProgressHandler(resetProgress),
764 });
765 cancellationToken.ThrowIfCancellationRequested();
766 libGitRepo.RemoveUntrackedFiles();
767 },
768 cancellationToken,
770 TaskScheduler.Current);
771 }
772
773 var remainingProgressFactor = 0.9;
774 if (!synchronizeTrackedBranch)
775 {
776 using var progressReporter2 = progressReporter.CreateSection("Push to temporary branch", remainingProgressFactor);
777 var credentialsHandler = await GenerateCredentialsHandler(username, password, cancellationToken);
778 await PushHeadToTemporaryBranch(
779 credentialsHandler,
780 progressReporter2,
781 cancellationToken);
782 return false;
783 }
784
785 var sameHead = Head == startHead;
786 if (sameHead || !Tracking)
787 {
788 logger.LogTrace("Aborted synchronize due to {abortReason}!", sameHead ? "lack of changes" : "not being on tracked reference");
789 return false;
790 }
791
792 logger.LogInformation("Synchronizing with origin...");
793 using var pushReporter = progressReporter.CreateSection("Push to origin", remainingProgressFactor);
794 var (pushOptions, progressReporters) = await GeneratePushOptions(
795 pushReporter,
796 username,
797 password,
798 cancellationToken);
799
800 return await Task.Factory.StartNew(
801 () =>
802 {
803 var remote = libGitRepo.Network.Remotes.First();
804 try
805 {
806 try
807 {
808 libGitRepo.Network.Push(
809 libGitRepo.Head,
810 pushOptions);
811 }
812 finally
813 {
814 foreach (var progressReporter in progressReporters)
815 progressReporter.Dispose();
816 }
817
818 return true;
819 }
820 catch (NonFastForwardException)
821 {
822 logger.LogInformation("Synchronize aborted, non-fast forward!");
823 return false;
824 }
825 catch (UserCancelledException e)
826 {
827 cancellationToken.ThrowIfCancellationRequested();
828 throw new InvalidOperationException("Caught UserCancelledException without cancellationToken triggering", e);
829 }
830 catch (LibGit2SharpException e)
831 {
832 logger.LogWarning(e, "Unable to make synchronization push!");
833 return false;
834 }
835 },
836 cancellationToken,
838 TaskScheduler.Current);
839 }
840
842 public Task<bool> IsSha(string committish, CancellationToken cancellationToken) => Task.Factory.StartNew(
843 () =>
844 {
845 // check if it's a tag
846 var gitObject = libGitRepo.Lookup(committish, ObjectType.Tag);
847 if (gitObject != null)
848 return false;
849 cancellationToken.ThrowIfCancellationRequested();
850
851 // check if it's a branch
852 if (libGitRepo.Branches[committish] != null)
853 return false;
854 cancellationToken.ThrowIfCancellationRequested();
855
856 // err on the side of references, if we can't look it up, assume its a reference
857 if (libGitRepo.Lookup<Commit>(committish) != null)
858 return true;
859 return false;
860 },
861 cancellationToken,
863 TaskScheduler.Current);
864
866 public Task<bool> CommittishIsParent(string committish, CancellationToken cancellationToken) => Task.Factory.StartNew(
867 () =>
868 {
869 var targetObject = libGitRepo.Lookup(committish);
870 if (targetObject == null)
871 {
872 logger.LogTrace("Committish {committish} not found in repository", committish);
873 return false;
874 }
875
876 if (targetObject is not Commit targetCommit)
877 {
878 if (targetObject is not TagAnnotation)
879 {
880 logger.LogTrace("Committish {committish} is a {type} and does not point to a commit!", committish, targetObject.GetType().Name);
881 return false;
882 }
883
884 targetCommit = targetObject.Peel<Commit>();
885 if (targetCommit == null)
886 {
887 logger.LogError(
888 "TagAnnotation {committish} was found but the commit associated with it could not be found in repository!",
889 committish);
890 return false;
891 }
892 }
893
894 cancellationToken.ThrowIfCancellationRequested();
895 var startSha = Head;
896 logger.LogTrace("Testing if {committish} is a parent of {startSha}...", committish, startSha);
897 MergeResult mergeResult;
898 try
899 {
900 mergeResult = libGitRepo.Merge(
901 targetCommit,
902 new Signature(
903 DefaultCommitterName,
904 DefaultCommitterEmail,
905 DateTimeOffset.UtcNow),
906 new MergeOptions
907 {
908 FastForwardStrategy = FastForwardStrategy.FastForwardOnly,
909 FailOnConflict = true,
910 });
911 }
912 catch (NonFastForwardException ex)
913 {
914 logger.LogTrace(ex, "{committish} is not a parent of {startSha}", committish, startSha);
915 return false;
916 }
917
918 if (mergeResult.Status == MergeStatus.UpToDate)
919 return true;
920
921 logger.LogTrace("{committish} is not a parent of {startSha} ({mergeStatus}). Moving back...", committish, startSha, mergeResult.Status);
922 commands.Checkout(
923 libGitRepo,
924 new CheckoutOptions
925 {
926 CheckoutModifiers = CheckoutModifiers.Force,
927 },
928 startSha);
929
930 return false;
931 },
932 cancellationToken,
934 TaskScheduler.Current);
935
937 public ValueTask<Models.TestMerge> GetTestMerge(
938 TestMergeParameters parameters,
939 RepositorySettings repositorySettings,
940 CancellationToken cancellationToken) => gitRemoteFeatures.GetTestMerge(
941 parameters,
942 repositorySettings,
943 cancellationToken);
944
946 public Task<DateTimeOffset> TimestampCommit(string sha, CancellationToken cancellationToken) => Task.Factory.StartNew(
947 () =>
948 {
949 ArgumentNullException.ThrowIfNull(sha);
950
951 var commit = libGitRepo.Lookup<Commit>(sha) ?? throw new JobException($"Commit {sha} does not exist in the repository!");
952 return commit.Committer.When.ToUniversalTime();
953 },
954 cancellationToken,
956 TaskScheduler.Current);
957
959 protected override void DisposeImpl()
960 {
961 logger.LogTrace("Disposing...");
962 libGitRepo.Dispose();
963 base.DisposeImpl();
964 }
965
973 void RawCheckout(string committish, bool moveCurrentReference, JobProgressReporter progressReporter, CancellationToken cancellationToken)
974 {
975 logger.LogTrace("Checkout: {committish}", committish);
976
977 var checkoutOptions = new CheckoutOptions
978 {
979 CheckoutModifiers = CheckoutModifiers.Force,
980 };
981
982 var stage = $"Checkout {committish}";
983 using var newProgressReporter = progressReporter.CreateSection(stage, 1.0);
984 newProgressReporter.ReportProgress(0);
985 checkoutOptions.OnCheckoutProgress = CheckoutProgressHandler(newProgressReporter);
986
987 cancellationToken.ThrowIfCancellationRequested();
988
989 if (moveCurrentReference)
990 {
991 if (Reference == NoReference)
992 throw new InvalidOperationException("Cannot move current reference when not on reference!");
993
994 var gitObject = libGitRepo.Lookup(committish);
995 if (gitObject == null)
996 throw new JobException($"Could not find committish: {committish}");
997
998 var commit = gitObject.Peel<Commit>();
999
1000 cancellationToken.ThrowIfCancellationRequested();
1001
1002 libGitRepo.Reset(ResetMode.Hard, commit, checkoutOptions);
1003 }
1004 else
1005 {
1006 void RunCheckout() => commands.Checkout(
1007 libGitRepo,
1008 checkoutOptions,
1009 committish);
1010
1011 try
1012 {
1013 RunCheckout();
1014 }
1015 catch (NotFoundException)
1016 {
1017 // Maybe (likely) a remote?
1018 var remoteName = $"origin/{committish}";
1019 var remoteBranch = libGitRepo.Branches.FirstOrDefault(
1020 branch => branch.FriendlyName.Equals(remoteName, StringComparison.Ordinal));
1021 cancellationToken.ThrowIfCancellationRequested();
1022
1023 if (remoteBranch == default)
1024 throw;
1025
1026 logger.LogDebug("Creating local branch for {remoteBranchFriendlyName}...", remoteBranch.FriendlyName);
1027 var branch = libGitRepo.CreateBranch(committish, remoteBranch.Tip);
1028
1029 libGitRepo.Branches.Update(branch, branchUpdate => branchUpdate.TrackedBranch = remoteBranch.CanonicalName);
1030
1031 cancellationToken.ThrowIfCancellationRequested();
1032
1033 RunCheckout();
1034 }
1035 }
1036
1037 cancellationToken.ThrowIfCancellationRequested();
1038
1039 libGitRepo.RemoveUntrackedFiles();
1040 }
1041
1049 Task PushHeadToTemporaryBranch(CredentialsHandler credentialsHandler, JobProgressReporter progressReporter, CancellationToken cancellationToken) => Task.Factory.StartNew(
1050 () =>
1051 {
1052 logger.LogInformation("Pushing changes to temporary remote branch...");
1053 var branch = libGitRepo.CreateBranch(RemoteTemporaryBranchName);
1054 try
1055 {
1056 cancellationToken.ThrowIfCancellationRequested();
1057 var remote = libGitRepo.Network.Remotes.First();
1058 try
1059 {
1060 var forcePushString = String.Format(CultureInfo.InvariantCulture, "+{0}:{0}", branch.CanonicalName);
1061
1062 using (var mainPushReporter = progressReporter.CreateSection(null, 0.9))
1063 {
1064 var (pushOptions, progressReporters) = GeneratePushOptions(
1065 mainPushReporter,
1066 credentialsHandler,
1067 cancellationToken);
1068
1069 try
1070 {
1071 libGitRepo.Network.Push(remote, forcePushString, pushOptions);
1072 }
1073 finally
1074 {
1075 foreach (var progressReporter in progressReporters)
1076 progressReporter.Dispose();
1077 }
1078 }
1079
1080 var removalString = String.Format(CultureInfo.InvariantCulture, ":{0}", branch.CanonicalName);
1081 using var forcePushReporter = progressReporter.CreateSection(null, 0.1);
1082 var (forcePushOptions, forcePushReporters) = GeneratePushOptions(forcePushReporter, credentialsHandler, cancellationToken);
1083 try
1084 {
1085 libGitRepo.Network.Push(remote, removalString, forcePushOptions);
1086 }
1087 finally
1088 {
1089 foreach (var subForcePushReporter in forcePushReporters)
1090 forcePushReporter.Dispose();
1091 }
1092 }
1093 catch (UserCancelledException)
1094 {
1095 cancellationToken.ThrowIfCancellationRequested();
1096 }
1097 catch (LibGit2SharpException e)
1098 {
1099 logger.LogWarning(e, "Unable to push to temporary branch!");
1100 }
1101 }
1102 finally
1103 {
1104 libGitRepo.Branches.Remove(branch);
1105 }
1106 },
1107 cancellationToken,
1109 TaskScheduler.Current);
1110
1119 async ValueTask<(PushOptions PushOptions, IEnumerable<JobProgressReporter> SubProgressReporters)> GeneratePushOptions(JobProgressReporter progressReporter, string username, string password, CancellationToken cancellationToken)
1120 => GeneratePushOptions(
1121 progressReporter,
1122 await GenerateCredentialsHandler(username, password, cancellationToken),
1123 cancellationToken);
1124
1130 => ioManager.GetDirectoryName(libGitRepo
1131 .Info
1132 .Path
1133 .TrimEnd(ioManager.DirectorySeparatorChar)
1134 .TrimEnd(ioManager.AltDirectorySeparatorChar));
1135
1146 JobProgressReporter progressReporter,
1147 string? username,
1148 string? password,
1149 bool deploymentPipeline,
1150 CancellationToken cancellationToken)
1151 {
1152 logger.LogTrace("Updating submodules {withOrWithout} credentials...", username == null ? "without" : "with");
1153
1154 async ValueTask RecursiveUpdateSubmodules(LibGit2Sharp.IRepository parentRepository, JobProgressReporter currentProgressReporter, string parentGitDirectory)
1155 {
1156 var submoduleCount = libGitRepo.Submodules.Count();
1157 if (submoduleCount == 0)
1158 {
1159 logger.LogTrace("No submodules, skipping update");
1160 return;
1161 }
1162
1163 var factor = 1.0 / submoduleCount / 3;
1164 foreach (var submodule in parentRepository.Submodules)
1165 {
1166 logger.LogTrace("Entering submodule {name} ({path}) for recursive updates...", submodule.Name, submodule.Path);
1167 var submoduleUpdateOptions = new SubmoduleUpdateOptions
1168 {
1169 Init = true,
1170 OnCheckoutNotify = (_, _) => !cancellationToken.IsCancellationRequested,
1171 };
1172
1173 using var fetchReporter = currentProgressReporter.CreateSection($"Fetch submodule {submodule.Name}", factor);
1174
1175 var credentialsHandler = await GenerateCredentialsHandler(username, password, cancellationToken);
1176
1177 submoduleUpdateOptions.FetchOptions.Hydrate(
1178 logger,
1179 fetchReporter,
1180 credentialsHandler,
1181 cancellationToken);
1182
1183 using var checkoutReporter = currentProgressReporter.CreateSection($"Checkout submodule {submodule.Name}", factor);
1184 submoduleUpdateOptions.OnCheckoutProgress = CheckoutProgressHandler(checkoutReporter);
1185
1186 logger.LogDebug("Updating submodule {submoduleName}...", submodule.Name);
1187 Task RawSubModuleUpdate() => Task.Factory.StartNew(
1188 () => parentRepository.Submodules.Update(submodule.Name, submoduleUpdateOptions),
1189 cancellationToken,
1191 TaskScheduler.Current);
1192
1193 try
1194 {
1195 await RawSubModuleUpdate();
1196 }
1197 catch (LibGit2SharpException ex) when (parentRepository == libGitRepo)
1198 {
1199 // workaround for https://github.com/libgit2/libgit2/issues/3820
1200 // kill off the modules/ folder in .git and try again
1201 currentProgressReporter.ReportProgress(0);
1202 credentialsProvider.CheckBadCredentialsException(ex);
1203 logger.LogWarning(ex, "Initial update of submodule {submoduleName} failed. Deleting submodule directories and re-attempting...", submodule.Name);
1204
1205 await Task.WhenAll(
1206 ioManager.DeleteDirectory($".git/modules/{submodule.Path}", cancellationToken),
1207 ioManager.DeleteDirectory(submodule.Path, cancellationToken));
1208
1209 logger.LogTrace("Second update attempt for submodule {submoduleName}...", submodule.Name);
1210 try
1211 {
1212 await RawSubModuleUpdate();
1213 }
1214 catch (UserCancelledException)
1215 {
1216 cancellationToken.ThrowIfCancellationRequested();
1217 }
1218 catch (LibGit2SharpException ex2)
1219 {
1220 credentialsProvider.CheckBadCredentialsException(ex2);
1221 logger.LogTrace(ex2, "Retried update of submodule {submoduleName} failed!", submodule.Name);
1222 throw new AggregateException(ex, ex2);
1223 }
1224 }
1225
1226 await eventConsumer.HandleEvent(
1227 EventType.RepoSubmoduleUpdate,
1228 new List<string> { submodule.Name },
1229 false,
1230 deploymentPipeline,
1231 cancellationToken);
1232
1233 var submodulePath = ioManager.ResolvePath(
1234 ioManager.ConcatPath(
1235 parentGitDirectory,
1236 submodule.Path));
1237
1238 using var submoduleRepo = await submoduleFactory.CreateFromPath(
1239 submodulePath,
1240 cancellationToken);
1241
1242 using var submoduleReporter = currentProgressReporter.CreateSection($"Entering submodule \"{submodule.Name}\"...", factor);
1243 await RecursiveUpdateSubmodules(
1244 submoduleRepo,
1245 submoduleReporter,
1246 submodulePath);
1247 }
1248 }
1249
1250 return RecursiveUpdateSubmodules(libGitRepo, progressReporter, GetRepositoryPath());
1251 }
1252
1260 ValueTask<CredentialsHandler> GenerateCredentialsHandler(string? username, string? password, CancellationToken cancellationToken)
1261 => credentialsProvider.GenerateCredentialsHandler(gitRemoteFeatures, username, password, cancellationToken);
1262
1268 CheckoutProgressHandler CheckoutProgressHandler(JobProgressReporter progressReporter) => (a, completedSteps, totalSteps) =>
1269 {
1270 double? percentage;
1271
1272 // short circuit initialization where totalSteps is 0
1273 if (completedSteps == 0)
1274 percentage = 0;
1275 else if (totalSteps < completedSteps || totalSteps == 0)
1276 percentage = null;
1277 else
1278 {
1279 percentage = ((double)completedSteps) / totalSteps;
1280 if (percentage < 0)
1281 percentage = null;
1282 }
1283
1284 if (percentage == null)
1285 logger.LogDebug(
1286 "Bad checkout progress values (Please tell Dominion)! Completeds: {completed}, Total: {total}",
1287 completedSteps,
1288 totalSteps);
1289
1290 progressReporter.ReportProgress(percentage);
1291 };
1292 }
1293#pragma warning restore CA1506
1294}
Represents configurable settings for a git repository.
virtual ? string TargetCommitSha
The sha of the test merge revision to merge. If not specified, the latest commit from the source will...
string? Comment
Optional comment about the test.
int Number
The number of the test merge source.
string Reference
The current reference the IRepository HEAD is using. This can be a branch or tag.
Definition Repository.cs:74
readonly ILibGit2Commands commands
The ILibGit2Commands for the Repository.
Definition Repository.cs:87
Task PushHeadToTemporaryBranch(CredentialsHandler credentialsHandler, JobProgressReporter progressReporter, CancellationToken cancellationToken)
Force push the current repository HEAD to RemoteTemporaryBranchName;.
const string OriginTrackingErrorTemplate
Template error message for when tracking of the most recent origin commit fails.
Definition Repository.cs:41
Task ResetToSha(string sha, JobProgressReporter progressReporter, CancellationToken cancellationToken)
Requires the current HEAD to be a reference. Hard resets the reference to the given sha....
const string DefaultCommitterEmail
The default password for committers.
Definition Repository.cs:36
readonly IOptionsMonitor< GeneralConfiguration > generalConfigurationOptions
The IOptionsMonitor<TOptions> of GeneralConfiguration for the Repository.
async ValueTask FetchOrigin(JobProgressReporter progressReporter, string? username, string? password, bool deploymentPipeline, CancellationToken cancellationToken)
Fetch commits from the origin repository.A ValueTask representing the running operation.
ValueTask< CredentialsHandler > GenerateCredentialsHandler(string? username, string? password, CancellationToken cancellationToken)
Generate a CredentialsHandler from a given username and password .
async ValueTask CopyTo(string path, CancellationToken cancellationToken)
Copies the current working directory to a given path .A ValueTask representing the running operation.
Repository(LibGit2Sharp.IRepository libGitRepo, ILibGit2Commands commands, IIOManager ioManager, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, IPostWriteHandler postWriteHandler, IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, ILibGit2RepositoryFactory submoduleFactory, IOptionsMonitor< GeneralConfiguration > generalConfigurationOptions, ILogger< Repository > logger, Action disposeAction)
Initializes a new instance of the Repository class.
async ValueTask< TestMergeResult > AddTestMerge(TestMergeParameters testMergeParameters, string committerName, string committerEmail, string? username, string? password, bool updateSubmodules, JobProgressReporter progressReporter, CancellationToken cancellationToken)
Attempt to merge the revision specified by a given set of testMergeParameters into HEAD....
string Head
The SHA of the IRepository HEAD.
Definition Repository.cs:71
string? RemoteRepositoryName
If RemoteGitProvider is not RemoteGitProvider.Unknown this will be set with the name of the repositor...
Definition Repository.cs:65
bool Tracking
If Reference tracks an upstream branch.
Definition Repository.cs:68
const string RemoteTemporaryBranchName
The branch name used for publishing testmerge commits.
Definition Repository.cs:46
async ValueTask< bool > Synchronize(JobProgressReporter progressReporter, string? username, string? password, string committerName, string committerEmail, bool synchronizeTrackedBranch, bool deploymentPipeline, CancellationToken cancellationToken)
Runs the synchronize event script and attempts to push any changes made to the IRepository if on a tr...
Uri Origin
The current origin remote the IRepository is using.
Definition Repository.cs:77
async ValueTask CheckoutObject(string committish, string? username, string? password, bool updateSubmodules, bool moveCurrentReference, JobProgressReporter progressReporter, CancellationToken cancellationToken)
Checks out a given committish .A ValueTask representing the running operation.
readonly LibGit2Sharp.IRepository libGitRepo
The LibGit2Sharp.IRepository for the Repository.
Definition Repository.cs:82
readonly IGitRemoteFeatures gitRemoteFeatures
The IGitRemoteFeatures for the Repository.
ValueTask UpdateSubmodules(JobProgressReporter progressReporter, string? username, string? password, bool deploymentPipeline, CancellationToken cancellationToken)
Recusively update all Submodules in the libGitRepo.
readonly ILibGit2RepositoryFactory submoduleFactory
The ILibGit2RepositoryFactory used for updating submodules.
async ValueTask< bool?> MergeOrigin(JobProgressReporter progressReporter, string committerName, string committerEmail, bool deploymentPipeline, CancellationToken cancellationToken)
Requires the current HEAD to be a tracked reference. Merges the reference to what it tracks on the or...
readonly ILogger< Repository > logger
The ILogger for the Repository.
override void DisposeImpl()
Implementation of Dispose run after reentrancy check.
async ValueTask<(PushOptions PushOptions, IEnumerable< JobProgressReporter > SubProgressReporters)> GeneratePushOptions(JobProgressReporter progressReporter, string username, string password, CancellationToken cancellationToken)
Generate a standard set of PushOptions.
static PushOptions PushOptions
Generate a standard set of PushOptions.
string? RemoteRepositoryOwner
If RemoteGitProvider is not RemoteGitProvider.Unknown this will be set with the owner of the reposito...
Definition Repository.cs:62
readonly IIOManager ioManager
The IIOManager for the Repository.
Definition Repository.cs:92
ValueTask< Models.TestMerge > GetTestMerge(TestMergeParameters parameters, RepositorySettings repositorySettings, CancellationToken cancellationToken)
Retrieve the Models.TestMerge representation of given test merge parameters .A ValueTask<TResult> res...
readonly IPostWriteHandler postWriteHandler
The IPostWriteHandler for the Repository.
const string DefaultCommitterName
The default username for committers.
Definition Repository.cs:31
const string UnknownReference
Used when a reference cannot be determined.
Definition Repository.cs:56
Task< string > GetOriginSha(CancellationToken cancellationToken)
Get the tracked reference's current SHA.A Task<TResult> resulting in the tracked origin reference's S...
Task< bool > IsSha(string committish, CancellationToken cancellationToken)
Checks if a given committish is a sha.A Task<TResult> resulting in true if committish is a sha,...
static PushOptions IEnumerable< JobProgressReporter > SubProgressReporters GeneratePushOptions(JobProgressReporter progressReporter, CredentialsHandler credentialsHandler, CancellationToken cancellationToken)
Task< bool > CommittishIsParent(string committish, CancellationToken cancellationToken)
Check if a given committish is a parent of the current Head.A Task<TResult> resulting in true if com...
string GetRepositoryPath()
Gets the path of libGitRepo.
readonly IEventConsumer eventConsumer
The IEventConsumer for the Repository.
Definition Repository.cs:97
const string NoReference
The value of Reference when not on a reference.
Definition Repository.cs:51
CheckoutProgressHandler CheckoutProgressHandler(JobProgressReporter progressReporter)
Converts a given progressReporter to a LibGit2Sharp.Handlers.CheckoutProgressHandler.
void RawCheckout(string committish, bool moveCurrentReference, JobProgressReporter progressReporter, CancellationToken cancellationToken)
Runs a blocking force checkout to committish .
readonly ICredentialsProvider credentialsProvider
The ICredentialsProvider for the Repository.
Task< DateTimeOffset > TimestampCommit(string sha, CancellationToken cancellationToken)
Gets the DateTimeOffset a given sha was created on.A Task<TResult> resulting in the DateTimeOffset t...
async ValueTask ResetToOrigin(JobProgressReporter progressReporter, string? username, string? password, bool updateSubmodules, bool deploymentPipeline, CancellationToken cancellationToken)
Requires the current HEAD to be a tracked reference. Hard resets the reference to what it tracks on t...
Represents the result of a repository test merge attempt.
IIOManager that resolves paths to Environment.CurrentDirectory.
const TaskCreationOptions BlockingTaskCreationOptions
The TaskCreationOptions used to spawn Tasks for potentially long running, blocking operations.
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.
Runs a given disposeAction on Dispose.
readonly Action disposeAction
The Action to run on Dispose.
string? RemoteRepositoryName
If RemoteGitProvider is not RemoteGitProvider.Unknown this will be set with the name of the repositor...
RemoteGitProvider? RemoteGitProvider
The Models.RemoteGitProvider in use by the repository.
string? RemoteRepositoryOwner
If RemoteGitProvider is not RemoteGitProvider.Unknown this will be set with the owner of the reposito...
Consumes EventTypes and takes the appropriate actions.
ValueTask HandleEvent(EventType eventType, IEnumerable< string?> parameters, bool sensitiveParameters, bool deploymentPipeline, CancellationToken cancellationToken)
Handle a given eventType .
void CheckBadCredentialsException(LibGit2SharpException exception)
Rethrow the authentication failure message as a JobException if it is one.
IGitRemoteFeatures CreateGitRemoteFeatures(IRepository repository)
Create the IGitRemoteFeatures for a given repository .
ValueTask< string?> TransformRepositoryPassword(string? rawPassword, CancellationToken cancellationToken)
Transform a service's rawPassword into a password usable by git.
string TestMergeRefSpecFormatter
Gets a formatter string which creates the remote refspec for fetching the HEAD of passed in test merg...
For low level interactions with a LibGit2Sharp.IRepository.
void Fetch(LibGit2Sharp.IRepository repository, IEnumerable< string > refSpecs, Remote remote, FetchOptions fetchOptions, string logMessage)
Runs a blocking fetch operation on a given repository .
Represents an on-disk git repository.
string Head
The SHA of the IRepository HEAD.
Interface for using filesystems.
Definition IIOManager.cs:14
string ResolvePath()
Retrieve the full path of the current working directory.
ValueTask CopyDirectory(IEnumerable< string >? ignore, Func< string, string, ValueTask >? postCopyCallback, string src, string dest, int? taskThrottle, CancellationToken cancellationToken)
Copies a directory from src to dest .
Handles changing file modes/permissions after writing.
void HandleWrite(string filePath)
For handling system specific necessities after a write.
bool NeedsPostWrite(string sourceFilePath)
Check if a given sourceFilePath will need HandleWrite(string) called on a copy of it.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
Definition ErrorCode.cs:12
RemoteGitProvider
Indicates the remote git host.
EventType
Types of events. Mirror in tgs.dm. Prefer last listed name for script.
Definition EventType.cs:7