tgstation-server 6.12.0
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.IO;
5using System.Linq;
6using System.Threading;
7using System.Threading.Tasks;
8
9using LibGit2Sharp;
10using LibGit2Sharp.Handlers;
11
12using Microsoft.Extensions.Logging;
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 ILogger<Repository> logger;
123
128
144 LibGit2Sharp.IRepository libGitRepo,
150 IGitRemoteFeaturesFactory gitRemoteFeaturesFactory,
152 ILogger<Repository> logger,
154 Action disposeAction)
155 : base(disposeAction)
156 {
157 this.libGitRepo = libGitRepo ?? throw new ArgumentNullException(nameof(libGitRepo));
158 this.commands = commands ?? throw new ArgumentNullException(nameof(commands));
159 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
160 this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
161 this.credentialsProvider = credentialsProvider ?? throw new ArgumentNullException(nameof(credentialsProvider));
162 this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
163 ArgumentNullException.ThrowIfNull(gitRemoteFeaturesFactory);
164 this.submoduleFactory = submoduleFactory ?? throw new ArgumentNullException(nameof(submoduleFactory));
165
166 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
167 this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration));
168
169 gitRemoteFeatures = gitRemoteFeaturesFactory.CreateGitRemoteFeatures(this);
170 }
171
173#pragma warning disable CA1506 // TODO: Decomplexify
174 public async ValueTask<TestMergeResult> AddTestMerge(
175 TestMergeParameters testMergeParameters,
176 string committerName,
177 string committerEmail,
178 string? username,
179 string? password,
180 bool updateSubmodules,
181 JobProgressReporter progressReporter,
182 CancellationToken cancellationToken)
183 {
184 ArgumentNullException.ThrowIfNull(testMergeParameters);
185 ArgumentNullException.ThrowIfNull(committerName);
186 ArgumentNullException.ThrowIfNull(committerEmail);
187 ArgumentNullException.ThrowIfNull(progressReporter);
188
189 logger.LogDebug(
190 "Begin AddTestMerge: #{prNumber} at {targetSha} ({comment}) by <{committerName} ({committerEmail})>",
191 testMergeParameters.Number,
192 testMergeParameters.TargetCommitSha?[..7],
193 testMergeParameters.Comment,
194 committerName,
195 committerEmail);
196
197 if (RemoteGitProvider == Api.Models.RemoteGitProvider.Unknown)
198 throw new InvalidOperationException("Cannot test merge with an Unknown RemoteGitProvider!");
199
200 var commitMessage = String.Format(
201 CultureInfo.InvariantCulture,
202 "TGS Test Merge (#{0}){1}{2}",
203 testMergeParameters.Number,
204 testMergeParameters.Comment != null
205 ? Environment.NewLine
206 : String.Empty,
207 testMergeParameters.Comment ?? String.Empty);
208
209 var testMergeBranchName = String.Format(CultureInfo.InvariantCulture, "tm-{0}", testMergeParameters.Number);
210 var localBranchName = String.Format(CultureInfo.InvariantCulture, gitRemoteFeatures.TestMergeLocalBranchNameFormatter, testMergeParameters.Number, testMergeBranchName);
211
212 var refSpec = String.Format(CultureInfo.InvariantCulture, gitRemoteFeatures.TestMergeRefSpecFormatter, testMergeParameters.Number, testMergeBranchName);
213 var refSpecList = new List<string> { refSpec };
214 var logMessage = String.Format(CultureInfo.InvariantCulture, "Test merge #{0}", testMergeParameters.Number);
215
216 var originalCommit = libGitRepo.Head;
217
218 MergeResult? result = null;
219
220 var progressFactor = 1.0 / (updateSubmodules ? 3 : 2);
221
222 var sig = new Signature(new Identity(committerName, committerEmail), DateTimeOffset.UtcNow);
223 List<string>? conflictedPaths = null;
224 await Task.Factory.StartNew(
225 () =>
226 {
227 try
228 {
229 try
230 {
231 logger.LogTrace("Fetching refspec {refSpec}...", refSpec);
232
233 var remote = libGitRepo.Network.Remotes.First();
234 using var fetchReporter = progressReporter.CreateSection($"Fetch {refSpec}", progressFactor);
237 refSpecList,
238 remote,
239 new FetchOptions().Hydrate(
240 logger,
241 fetchReporter,
243 cancellationToken),
244 logMessage);
245 }
246 catch (UserCancelledException ex)
247 {
248 logger.LogTrace(ex, "Suppressing fetch cancel exception");
249 }
250 catch (LibGit2SharpException ex)
251 {
253 }
254
255 cancellationToken.ThrowIfCancellationRequested();
256
257 libGitRepo.RemoveUntrackedFiles();
258
259 cancellationToken.ThrowIfCancellationRequested();
260
261 var objectName = testMergeParameters.TargetCommitSha ?? localBranchName;
262 var gitObject = libGitRepo.Lookup(objectName) ?? throw new JobException($"Could not find object to merge: {objectName}");
263
264 testMergeParameters.TargetCommitSha = gitObject.Sha;
265
266 cancellationToken.ThrowIfCancellationRequested();
267
268 logger.LogTrace("Merging {targetCommitSha} into {currentReference}...", testMergeParameters.TargetCommitSha[..7], Reference);
269
270 using var mergeReporter = progressReporter.CreateSection($"Merge {testMergeParameters.TargetCommitSha[..7]}", progressFactor);
271 result = libGitRepo.Merge(testMergeParameters.TargetCommitSha, sig, new MergeOptions
272 {
273 CommitOnSuccess = commitMessage == null,
274 FailOnConflict = false, // Needed to get conflicting files
275 FastForwardStrategy = FastForwardStrategy.NoFastForward,
276 SkipReuc = true,
277 OnCheckoutProgress = CheckoutProgressHandler(mergeReporter),
278 });
279 }
280 finally
281 {
282 libGitRepo.Branches.Remove(localBranchName);
283 }
284
285 cancellationToken.ThrowIfCancellationRequested();
286
287 if (result.Status == MergeStatus.Conflicts)
288 {
289 var repoStatus = libGitRepo.RetrieveStatus();
290 conflictedPaths = new List<string>();
291 foreach (var file in repoStatus)
292 if (file.State == FileStatus.Conflicted)
293 conflictedPaths.Add(file.FilePath);
294
295 var revertTo = originalCommit.CanonicalName ?? originalCommit.Tip.Sha;
296 logger.LogDebug("Merge conflict, aborting and reverting to {revertTarget}", revertTo);
297 progressReporter.ReportProgress(0);
298 using var revertReporter = progressReporter.CreateSection("Hard Reset to {revertTo}", 1.0);
299 RawCheckout(revertTo, false, revertReporter, cancellationToken);
300 cancellationToken.ThrowIfCancellationRequested();
301 }
302
303 libGitRepo.RemoveUntrackedFiles();
304 },
305 cancellationToken,
307 TaskScheduler.Current);
308
309 if (result!.Status == MergeStatus.Conflicts)
310 {
311 var arguments = new List<string>
312 {
313 originalCommit.Tip.Sha,
314 testMergeParameters.TargetCommitSha!,
315 originalCommit.FriendlyName ?? UnknownReference,
316 testMergeBranchName,
317 };
318
319 arguments.AddRange(conflictedPaths!);
320
322 EventType.RepoMergeConflict,
323 arguments,
324 false,
325 cancellationToken);
326 return new TestMergeResult
327 {
328 Status = result.Status,
329 ConflictingFiles = conflictedPaths,
330 };
331 }
332
333 if (result.Status != MergeStatus.UpToDate)
334 {
335 logger.LogTrace("Committing merge: \"{commitMessage}\"...", commitMessage);
336 await Task.Factory.StartNew(
337 () => libGitRepo.Commit(commitMessage, sig, sig, new CommitOptions
338 {
339 PrettifyMessage = true,
340 }),
341 cancellationToken,
343 TaskScheduler.Current);
344
345 if (updateSubmodules)
346 {
347 using var progressReporter2 = progressReporter.CreateSection("Update Submodules", progressFactor);
348 await UpdateSubmodules(
349 progressReporter2,
350 username,
351 password,
352 false,
353 cancellationToken);
354 }
355 }
356
358 EventType.RepoAddTestMerge,
359 new List<string?>
360 {
361 testMergeParameters.Number.ToString(CultureInfo.InvariantCulture),
362 testMergeParameters.TargetCommitSha!,
363 testMergeParameters.Comment,
364 },
365 false,
366 cancellationToken);
367
368 return new TestMergeResult
369 {
370 Status = result.Status,
371 };
372 }
373#pragma warning restore CA1506
374
376 public async ValueTask CheckoutObject(
377 string committish,
378 string? username,
379 string? password,
380 bool updateSubmodules,
381 bool moveCurrentReference,
382 JobProgressReporter progressReporter,
383 CancellationToken cancellationToken)
384 {
385 ArgumentNullException.ThrowIfNull(committish);
386
387 logger.LogDebug("Checkout object: {committish}...", committish);
388 await eventConsumer.HandleEvent(EventType.RepoCheckout, new List<string> { committish, moveCurrentReference.ToString() }, false, cancellationToken);
389 await Task.Factory.StartNew(
390 () =>
391 {
392 libGitRepo.RemoveUntrackedFiles();
393 using var progressReporter3 = progressReporter.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0);
395 committish,
396 moveCurrentReference,
397 progressReporter3,
398 cancellationToken);
399 },
400 cancellationToken,
402 TaskScheduler.Current);
403
404 if (updateSubmodules)
405 {
406 using var progressReporter2 = progressReporter.CreateSection(null, 1.0 / 3);
407 await UpdateSubmodules(
408 progressReporter2,
409 username,
410 password,
411 false,
412 cancellationToken);
413 }
414 }
415
417 public async ValueTask FetchOrigin(
418 JobProgressReporter progressReporter,
419 string? username,
420 string? password,
421 bool deploymentPipeline,
422 CancellationToken cancellationToken)
423 {
424 logger.LogDebug("Fetch origin...");
425 await eventConsumer.HandleEvent(EventType.RepoFetch, Enumerable.Empty<string>(), deploymentPipeline, cancellationToken);
426 await Task.Factory.StartNew(
427 () =>
428 {
429 var remote = libGitRepo.Network.Remotes.First();
430 try
431 {
432 using var subReporter = progressReporter.CreateSection("Fetch Origin", 1.0);
433 var fetchOptions = new FetchOptions
434 {
435 Prune = true,
436 TagFetchMode = TagFetchMode.All,
437 }.Hydrate(
438 logger,
439 subReporter,
441 cancellationToken);
442
445 remote
446 .FetchRefSpecs
447 .Select(x => x.Specification),
448 remote,
449 fetchOptions,
450 "Fetch origin commits");
451 }
452 catch (UserCancelledException)
453 {
454 cancellationToken.ThrowIfCancellationRequested();
455 }
456 catch (LibGit2SharpException ex)
457 {
459 }
460 },
461 cancellationToken,
463 TaskScheduler.Current);
464 }
465
467 public async ValueTask ResetToOrigin(
468 JobProgressReporter progressReporter,
469 string? username,
470 string? password,
471 bool updateSubmodules,
472 bool deploymentPipeline,
473 CancellationToken cancellationToken)
474 {
475 ArgumentNullException.ThrowIfNull(progressReporter);
476 if (!Tracking)
477 throw new JobException(ErrorCode.RepoReferenceRequired);
478 logger.LogTrace("Reset to origin...");
479 var trackedBranch = libGitRepo.Head.TrackedBranch;
480 await eventConsumer.HandleEvent(EventType.RepoResetOrigin, new List<string> { trackedBranch.FriendlyName, trackedBranch.Tip.Sha }, deploymentPipeline, cancellationToken);
481
482 using (var progressReporter2 = progressReporter.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0))
483 await ResetToSha(
484 trackedBranch.Tip.Sha,
485 progressReporter2,
486 cancellationToken);
487
488 if (updateSubmodules)
489 {
490 using var progressReporter3 = progressReporter.CreateSection(null, 1.0 / 3);
491 await UpdateSubmodules(
492 progressReporter3,
493 username,
494 password,
495 deploymentPipeline,
496 cancellationToken);
497 }
498 }
499
501 public Task ResetToSha(string sha, JobProgressReporter progressReporter, CancellationToken cancellationToken) => Task.Factory.StartNew(
502 () =>
503 {
504 ArgumentNullException.ThrowIfNull(sha);
505 ArgumentNullException.ThrowIfNull(progressReporter);
506
507 logger.LogDebug("Reset to sha: {sha}", sha[..7]);
508
509 libGitRepo.RemoveUntrackedFiles();
510 cancellationToken.ThrowIfCancellationRequested();
511
512 var gitObject = libGitRepo.Lookup(sha, ObjectType.Commit);
513 cancellationToken.ThrowIfCancellationRequested();
514
515 if (gitObject == null)
516 throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Cannot reset to non-existent SHA: {0}", sha));
517
518 libGitRepo.Reset(ResetMode.Hard, gitObject.Peel<Commit>(), new CheckoutOptions
519 {
520 OnCheckoutProgress = CheckoutProgressHandler(progressReporter.CreateSection($"Reset to {gitObject.Sha}", 1.0)),
521 });
522 },
523 cancellationToken,
525 TaskScheduler.Current);
526
528 public async ValueTask CopyTo(string path, CancellationToken cancellationToken)
529 {
530 ArgumentNullException.ThrowIfNull(path);
531 logger.LogTrace("Copying to {path}...", path);
533 new List<string> { ".git" },
534 (src, dest) =>
535 {
538
539 return ValueTask.CompletedTask;
540 },
542 path,
543 generalConfiguration.GetCopyDirectoryTaskThrottle(),
544 cancellationToken);
545 }
546
548 public Task<string> GetOriginSha(CancellationToken cancellationToken) => Task.Factory.StartNew(
549 () =>
550 {
551 if (!Tracking)
552 throw new JobException(ErrorCode.RepoReferenceRequired);
553
554 cancellationToken.ThrowIfCancellationRequested();
555
556 return libGitRepo.Head.TrackedBranch.Tip.Sha;
557 },
558 cancellationToken,
560 TaskScheduler.Current);
561
563 public async ValueTask<bool?> MergeOrigin(
564 JobProgressReporter progressReporter,
565 string committerName,
566 string committerEmail,
567 bool deploymentPipeline,
568 CancellationToken cancellationToken)
569 {
570 ArgumentNullException.ThrowIfNull(progressReporter);
571
572 MergeResult? result = null;
573 Branch? trackedBranch = null;
574
575 var oldHead = libGitRepo.Head;
576 var oldTip = oldHead.Tip;
577
578 await Task.Factory.StartNew(
579 () =>
580 {
581 if (!Tracking)
582 throw new JobException(ErrorCode.RepoReferenceRequired);
583
584 libGitRepo.RemoveUntrackedFiles();
585
586 cancellationToken.ThrowIfCancellationRequested();
587
588 trackedBranch = libGitRepo.Head.TrackedBranch;
589 logger.LogDebug(
590 "Merge origin/{trackedBranch}: <{committerName} ({committerEmail})>",
591 trackedBranch.FriendlyName,
592 committerName,
593 committerEmail);
594 result = libGitRepo.Merge(trackedBranch, new Signature(committerName, committerEmail, DateTimeOffset.UtcNow), new MergeOptions
595 {
596 CommitOnSuccess = true,
597 FailOnConflict = true,
598 FastForwardStrategy = FastForwardStrategy.Default,
599 SkipReuc = true,
600 OnCheckoutProgress = CheckoutProgressHandler(progressReporter.CreateSection("Merge Origin", 1.0)),
601 });
602
603 cancellationToken.ThrowIfCancellationRequested();
604
605 if (result.Status == MergeStatus.Conflicts)
606 {
607 logger.LogDebug("Merge conflict, aborting and reverting to {oldHeadFriendlyName}", oldHead.FriendlyName);
608 progressReporter.ReportProgress(0);
609 libGitRepo.Reset(ResetMode.Hard, oldTip, new CheckoutOptions
610 {
611 OnCheckoutProgress = CheckoutProgressHandler(progressReporter.CreateSection($"Hard Reset to {oldHead.FriendlyName}", 1.0)),
612 });
613 cancellationToken.ThrowIfCancellationRequested();
614 }
615
616 libGitRepo.RemoveUntrackedFiles();
617 },
618 cancellationToken,
620 TaskScheduler.Current);
621
622 if (result!.Status == MergeStatus.Conflicts)
623 {
625 EventType.RepoMergeConflict,
626 new List<string>
627 {
628 oldTip.Sha,
629 trackedBranch!.Tip.Sha,
630 oldHead.FriendlyName ?? UnknownReference,
631 trackedBranch.FriendlyName,
632 },
633 deploymentPipeline,
634 cancellationToken);
635 return null;
636 }
637
638 return result.Status == MergeStatus.FastForward;
639 }
640
642 public async ValueTask<bool> Synchronize(
643 JobProgressReporter progressReporter,
644 string? username,
645 string? password,
646 string committerName,
647 string committerEmail,
648 bool synchronizeTrackedBranch,
649 bool deploymentPipeline,
650 CancellationToken cancellationToken)
651 {
652 ArgumentNullException.ThrowIfNull(committerName);
653 ArgumentNullException.ThrowIfNull(committerEmail);
654 ArgumentNullException.ThrowIfNull(progressReporter);
655
656 if (username == null && password == null)
657 {
658 logger.LogTrace("Not synchronizing due to lack of credentials!");
659 return false;
660 }
661
662 logger.LogTrace("Begin Synchronize...");
663
664 ArgumentNullException.ThrowIfNull(username);
665 ArgumentNullException.ThrowIfNull(password);
666
667 var startHead = Head;
668
669 logger.LogTrace("Configuring <{committerName} ({committerEmail})> as author/committer", committerName, committerEmail);
670 await Task.Factory.StartNew(
671 () =>
672 {
673 libGitRepo.Config.Set("user.name", committerName);
674 cancellationToken.ThrowIfCancellationRequested();
675 libGitRepo.Config.Set("user.email", committerEmail);
676 },
677 cancellationToken,
679 TaskScheduler.Current);
680
681 cancellationToken.ThrowIfCancellationRequested();
682 try
683 {
684 await eventConsumer.HandleEvent(
685 EventType.RepoPreSynchronize,
686 new List<string>
687 {
688 ioManager.ResolvePath(),
689 },
690 deploymentPipeline,
691 cancellationToken);
692 }
693 finally
694 {
695 logger.LogTrace("Resetting and cleaning untracked files...");
696 await Task.Factory.StartNew(
697 () =>
698 {
699 using var resetProgress = progressReporter.CreateSection("Hard reset and remove untracked files", 0.1);
700 libGitRepo.Reset(ResetMode.Hard, libGitRepo.Head.Tip, new CheckoutOptions
701 {
702 OnCheckoutProgress = CheckoutProgressHandler(resetProgress),
703 });
704 cancellationToken.ThrowIfCancellationRequested();
705 libGitRepo.RemoveUntrackedFiles();
706 },
707 cancellationToken,
709 TaskScheduler.Current);
710 }
711
712 var remainingProgressFactor = 0.9;
713 if (!synchronizeTrackedBranch)
714 {
715 using var progressReporter2 = progressReporter.CreateSection("Push to temporary branch", remainingProgressFactor);
716 await PushHeadToTemporaryBranch(
717 username,
718 password,
719 progressReporter2,
720 cancellationToken);
721 return false;
722 }
723
724 var sameHead = Head == startHead;
725 if (sameHead || !Tracking)
726 {
727 logger.LogTrace("Aborted synchronize due to {abortReason}!", sameHead ? "lack of changes" : "not being on tracked reference");
728 return false;
729 }
730
731 logger.LogInformation("Synchronizing with origin...");
732
733 return await Task.Factory.StartNew(
734 () =>
735 {
736 var remote = libGitRepo.Network.Remotes.First();
737 try
738 {
739 using var pushReporter = progressReporter.CreateSection("Push to origin", remainingProgressFactor);
740 var (pushOptions, progressReporters) = GeneratePushOptions(
741 pushReporter,
742 username,
743 password,
744 cancellationToken);
745 try
746 {
747 libGitRepo.Network.Push(
748 libGitRepo.Head,
749 pushOptions);
750 }
751 finally
752 {
753 foreach (var progressReporter in progressReporters)
754 progressReporter.Dispose();
755 }
756
757 return true;
758 }
759 catch (NonFastForwardException)
760 {
761 logger.LogInformation("Synchronize aborted, non-fast forward!");
762 return false;
763 }
764 catch (UserCancelledException e)
765 {
766 cancellationToken.ThrowIfCancellationRequested();
767 throw new InvalidOperationException("Caught UserCancelledException without cancellationToken triggering", e);
768 }
769 catch (LibGit2SharpException e)
770 {
771 logger.LogWarning(e, "Unable to make synchronization push!");
772 return false;
773 }
774 },
775 cancellationToken,
777 TaskScheduler.Current);
778 }
779
781 public Task<bool> IsSha(string committish, CancellationToken cancellationToken) => Task.Factory.StartNew(
782 () =>
783 {
784 // check if it's a tag
785 var gitObject = libGitRepo.Lookup(committish, ObjectType.Tag);
786 if (gitObject != null)
787 return false;
788 cancellationToken.ThrowIfCancellationRequested();
789
790 // check if it's a branch
791 if (libGitRepo.Branches[committish] != null)
792 return false;
793 cancellationToken.ThrowIfCancellationRequested();
794
795 // err on the side of references, if we can't look it up, assume its a reference
796 if (libGitRepo.Lookup<Commit>(committish) != null)
797 return true;
798 return false;
799 },
800 cancellationToken,
802 TaskScheduler.Current);
803
805 public Task<bool> CommittishIsParent(string committish, CancellationToken cancellationToken) => Task.Factory.StartNew(
806 () =>
807 {
808 var targetObject = libGitRepo.Lookup(committish);
809 if (targetObject == null)
810 {
811 logger.LogTrace("Committish {committish} not found in repository", committish);
812 return false;
813 }
814
815 if (targetObject is not Commit targetCommit)
816 {
817 if (targetObject is not TagAnnotation)
818 {
819 logger.LogTrace("Committish {committish} is a {type} and does not point to a commit!", committish, targetObject.GetType().Name);
820 return false;
821 }
822
823 targetCommit = targetObject.Peel<Commit>();
824 if (targetCommit == null)
825 {
826 logger.LogError(
827 "TagAnnotation {committish} was found but the commit associated with it could not be found in repository!",
828 committish);
829 return false;
830 }
831 }
832
833 cancellationToken.ThrowIfCancellationRequested();
834 var startSha = Head;
835 logger.LogTrace("Testing if {committish} is a parent of {startSha}...", committish, startSha);
836 MergeResult mergeResult;
837 try
838 {
839 mergeResult = libGitRepo.Merge(
840 targetCommit,
841 new Signature(
842 DefaultCommitterName,
843 DefaultCommitterEmail,
844 DateTimeOffset.UtcNow),
845 new MergeOptions
846 {
847 FastForwardStrategy = FastForwardStrategy.FastForwardOnly,
848 FailOnConflict = true,
849 });
850 }
851 catch (NonFastForwardException ex)
852 {
853 logger.LogTrace(ex, "{committish} is not a parent of {startSha}", committish, startSha);
854 return false;
855 }
856
857 if (mergeResult.Status == MergeStatus.UpToDate)
858 return true;
859
860 logger.LogTrace("{committish} is not a parent of {startSha} ({mergeStatus}). Moving back...", committish, startSha, mergeResult.Status);
861 commands.Checkout(
862 libGitRepo,
863 new CheckoutOptions
864 {
865 CheckoutModifiers = CheckoutModifiers.Force,
866 },
867 startSha);
868
869 return false;
870 },
871 cancellationToken,
873 TaskScheduler.Current);
874
876 public ValueTask<Models.TestMerge> GetTestMerge(
877 TestMergeParameters parameters,
878 RepositorySettings repositorySettings,
879 CancellationToken cancellationToken) => gitRemoteFeatures.GetTestMerge(
880 parameters,
881 repositorySettings,
882 cancellationToken);
883
885 public Task<DateTimeOffset> TimestampCommit(string sha, CancellationToken cancellationToken) => Task.Factory.StartNew(
886 () =>
887 {
888 ArgumentNullException.ThrowIfNull(sha);
889
890 var commit = libGitRepo.Lookup<Commit>(sha) ?? throw new JobException($"Commit {sha} does not exist in the repository!");
891 return commit.Committer.When.ToUniversalTime();
892 },
893 cancellationToken,
895 TaskScheduler.Current);
896
898 protected override void DisposeImpl()
899 {
900 logger.LogTrace("Disposing...");
901 libGitRepo.Dispose();
902 base.DisposeImpl();
903 }
904
912 void RawCheckout(string committish, bool moveCurrentReference, JobProgressReporter progressReporter, CancellationToken cancellationToken)
913 {
914 logger.LogTrace("Checkout: {committish}", committish);
915
916 var checkoutOptions = new CheckoutOptions
917 {
918 CheckoutModifiers = CheckoutModifiers.Force,
919 };
920
921 var stage = $"Checkout {committish}";
922 using var newProgressReporter = progressReporter.CreateSection(stage, 1.0);
923 newProgressReporter.ReportProgress(0);
924 checkoutOptions.OnCheckoutProgress = CheckoutProgressHandler(newProgressReporter);
925
926 cancellationToken.ThrowIfCancellationRequested();
927
928 if (moveCurrentReference)
929 {
930 if (Reference == NoReference)
931 throw new InvalidOperationException("Cannot move current reference when not on reference!");
932
933 var gitObject = libGitRepo.Lookup(committish);
934 if (gitObject == null)
935 throw new JobException($"Could not find committish: {committish}");
936
937 var commit = gitObject.Peel<Commit>();
938
939 cancellationToken.ThrowIfCancellationRequested();
940
941 libGitRepo.Reset(ResetMode.Hard, commit, checkoutOptions);
942 }
943 else
944 {
945 void RunCheckout() => commands.Checkout(
946 libGitRepo,
947 checkoutOptions,
948 committish);
949
950 try
951 {
952 RunCheckout();
953 }
954 catch (NotFoundException)
955 {
956 // Maybe (likely) a remote?
957 var remoteName = $"origin/{committish}";
958 var remoteBranch = libGitRepo.Branches.FirstOrDefault(
959 branch => branch.FriendlyName.Equals(remoteName, StringComparison.Ordinal));
960 cancellationToken.ThrowIfCancellationRequested();
961
962 if (remoteBranch == default)
963 throw;
964
965 logger.LogDebug("Creating local branch for {remoteBranchFriendlyName}...", remoteBranch.FriendlyName);
966 var branch = libGitRepo.CreateBranch(committish, remoteBranch.Tip);
967
968 libGitRepo.Branches.Update(branch, branchUpdate => branchUpdate.TrackedBranch = remoteBranch.CanonicalName);
969
970 cancellationToken.ThrowIfCancellationRequested();
971
972 RunCheckout();
973 }
974 }
975
976 cancellationToken.ThrowIfCancellationRequested();
977
978 libGitRepo.RemoveUntrackedFiles();
979 }
980
989 Task PushHeadToTemporaryBranch(string username, string password, JobProgressReporter progressReporter, CancellationToken cancellationToken) => Task.Factory.StartNew(
990 () =>
991 {
992 logger.LogInformation("Pushing changes to temporary remote branch...");
993 var branch = libGitRepo.CreateBranch(RemoteTemporaryBranchName);
994 try
995 {
996 cancellationToken.ThrowIfCancellationRequested();
997 var remote = libGitRepo.Network.Remotes.First();
998 try
999 {
1000 var forcePushString = String.Format(CultureInfo.InvariantCulture, "+{0}:{0}", branch.CanonicalName);
1001
1002 using (var mainPushReporter = progressReporter.CreateSection(null, 0.9))
1003 {
1004 var (pushOptions, progressReporters) = GeneratePushOptions(
1005 mainPushReporter,
1006 username,
1007 password,
1008 cancellationToken);
1009
1010 try
1011 {
1012 libGitRepo.Network.Push(remote, forcePushString, pushOptions);
1013 }
1014 finally
1015 {
1016 foreach (var progressReporter in progressReporters)
1017 progressReporter.Dispose();
1018 }
1019 }
1020
1021 var removalString = String.Format(CultureInfo.InvariantCulture, ":{0}", branch.CanonicalName);
1022 using var forcePushReporter = progressReporter.CreateSection(null, 0.1);
1023 var (forcePushOptions, forcePushReporters) = GeneratePushOptions(forcePushReporter, username, password, cancellationToken);
1024 try
1025 {
1026 libGitRepo.Network.Push(remote, removalString, forcePushOptions);
1027 }
1028 finally
1029 {
1030 foreach (var subForcePushReporter in forcePushReporters)
1031 forcePushReporter.Dispose();
1032 }
1033 }
1034 catch (UserCancelledException)
1035 {
1036 cancellationToken.ThrowIfCancellationRequested();
1037 }
1038 catch (LibGit2SharpException e)
1039 {
1040 logger.LogWarning(e, "Unable to push to temporary branch!");
1041 }
1042 }
1043 finally
1044 {
1045 libGitRepo.Branches.Remove(branch);
1046 }
1047 },
1048 cancellationToken,
1050 TaskScheduler.Current);
1051
1060 (PushOptions PushOptions, IEnumerable<JobProgressReporter> SubProgressReporters) GeneratePushOptions(JobProgressReporter progressReporter, string username, string password, CancellationToken cancellationToken)
1061 {
1062 var packFileCountingReporter = progressReporter.CreateSection(null, 0.25);
1063 var packFileDeltafyingReporter = progressReporter.CreateSection(null, 0.25);
1064 var transferProgressReporter = progressReporter.CreateSection(null, 0.5);
1065
1066 return (
1067 PushOptions: new PushOptions
1068 {
1069 OnPackBuilderProgress = (stage, current, total) =>
1070 {
1071 if (total < current)
1072 total = current;
1073
1074 var percentage = ((double)current) / total;
1075 (stage == PackBuilderStage.Counting ? packFileCountingReporter : packFileDeltafyingReporter).ReportProgress(percentage);
1076 return !cancellationToken.IsCancellationRequested;
1077 },
1078 OnNegotiationCompletedBeforePush = (a) => !cancellationToken.IsCancellationRequested,
1079 OnPushTransferProgress = (a, sentBytes, totalBytes) =>
1080 {
1081 packFileCountingReporter.ReportProgress((double)sentBytes / totalBytes);
1082 return !cancellationToken.IsCancellationRequested;
1083 },
1084 CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password),
1085 },
1086 SubProgressReporters: new List<JobProgressReporter>
1087 {
1088 packFileCountingReporter,
1089 packFileDeltafyingReporter,
1090 transferProgressReporter,
1091 });
1092 }
1093
1099 => ioManager.GetDirectoryName(libGitRepo
1100 .Info
1101 .Path
1102 .TrimEnd(Path.DirectorySeparatorChar)
1103 .TrimEnd(Path.AltDirectorySeparatorChar));
1104
1115 JobProgressReporter progressReporter,
1116 string? username,
1117 string? password,
1118 bool deploymentPipeline,
1119 CancellationToken cancellationToken)
1120 {
1121 logger.LogTrace("Updating submodules {withOrWithout} credentials...", username == null ? "without" : "with");
1122
1123 async ValueTask RecursiveUpdateSubmodules(LibGit2Sharp.IRepository parentRepository, JobProgressReporter currentProgressReporter, string parentGitDirectory)
1124 {
1125 var submoduleCount = libGitRepo.Submodules.Count();
1126 if (submoduleCount == 0)
1127 {
1128 logger.LogTrace("No submodules, skipping update");
1129 return;
1130 }
1131
1132 var factor = 1.0 / submoduleCount / 3;
1133 foreach (var submodule in parentRepository.Submodules)
1134 {
1135 logger.LogTrace("Entering submodule {name} ({path}) for recursive updates...", submodule.Name, submodule.Path);
1136 var submoduleUpdateOptions = new SubmoduleUpdateOptions
1137 {
1138 Init = true,
1139 OnCheckoutNotify = (_, _) => !cancellationToken.IsCancellationRequested,
1140 };
1141
1142 using var fetchReporter = currentProgressReporter.CreateSection($"Fetch submodule {submodule.Name}", factor);
1143
1144 submoduleUpdateOptions.FetchOptions.Hydrate(
1145 logger,
1146 fetchReporter,
1147 credentialsProvider.GenerateCredentialsHandler(username, password),
1148 cancellationToken);
1149
1150 using var checkoutReporter = currentProgressReporter.CreateSection($"Checkout submodule {submodule.Name}", factor);
1151 submoduleUpdateOptions.OnCheckoutProgress = CheckoutProgressHandler(checkoutReporter);
1152
1153 logger.LogDebug("Updating submodule {submoduleName}...", submodule.Name);
1154 Task RawSubModuleUpdate() => Task.Factory.StartNew(
1155 () => parentRepository.Submodules.Update(submodule.Name, submoduleUpdateOptions),
1156 cancellationToken,
1158 TaskScheduler.Current);
1159
1160 try
1161 {
1162 await RawSubModuleUpdate();
1163 }
1164 catch (LibGit2SharpException ex) when (parentRepository == libGitRepo)
1165 {
1166 // workaround for https://github.com/libgit2/libgit2/issues/3820
1167 // kill off the modules/ folder in .git and try again
1168 currentProgressReporter.ReportProgress(0);
1169 credentialsProvider.CheckBadCredentialsException(ex);
1170 logger.LogWarning(ex, "Initial update of submodule {submoduleName} failed. Deleting submodule directories and re-attempting...", submodule.Name);
1171
1172 await Task.WhenAll(
1173 ioManager.DeleteDirectory($".git/modules/{submodule.Path}", cancellationToken),
1174 ioManager.DeleteDirectory(submodule.Path, cancellationToken));
1175
1176 logger.LogTrace("Second update attempt for submodule {submoduleName}...", submodule.Name);
1177 try
1178 {
1179 await RawSubModuleUpdate();
1180 }
1181 catch (UserCancelledException)
1182 {
1183 cancellationToken.ThrowIfCancellationRequested();
1184 }
1185 catch (LibGit2SharpException ex2)
1186 {
1187 credentialsProvider.CheckBadCredentialsException(ex2);
1188 logger.LogTrace(ex2, "Retried update of submodule {submoduleName} failed!", submodule.Name);
1189 throw new AggregateException(ex, ex2);
1190 }
1191 }
1192
1193 await eventConsumer.HandleEvent(
1194 EventType.RepoSubmoduleUpdate,
1195 new List<string> { submodule.Name },
1196 deploymentPipeline,
1197 cancellationToken);
1198
1199 var submodulePath = ioManager.ResolvePath(
1200 ioManager.ConcatPath(
1201 parentGitDirectory,
1202 submodule.Path));
1203
1204 using var submoduleRepo = await submoduleFactory.CreateFromPath(
1205 submodulePath,
1206 cancellationToken);
1207
1208 using var submoduleReporter = currentProgressReporter.CreateSection($"Entering submodule \"{submodule.Name}\"...", factor);
1209 await RecursiveUpdateSubmodules(
1210 submoduleRepo,
1211 submoduleReporter,
1212 submodulePath);
1213 }
1214 }
1215
1216 return RecursiveUpdateSubmodules(libGitRepo, progressReporter, GetRepositoryPath());
1217 }
1218
1224 CheckoutProgressHandler CheckoutProgressHandler(JobProgressReporter progressReporter) => (a, completedSteps, totalSteps) =>
1225 {
1226 double? percentage;
1227
1228 // short circuit initialization where totalSteps is 0
1229 if (completedSteps == 0)
1230 percentage = 0;
1231 else if (totalSteps < completedSteps || totalSteps == 0)
1232 percentage = null;
1233 else
1234 {
1235 percentage = ((double)completedSteps) / totalSteps;
1236 if (percentage < 0)
1237 percentage = null;
1238 }
1239
1240 if (percentage == null)
1241 logger.LogDebug(
1242 "Bad checkout progress values (Please tell Dominion)! Completeds: {completed}, Total: {total}",
1243 completedSteps,
1244 totalSteps);
1245
1246 progressReporter.ReportProgress(percentage);
1247 };
1248 }
1249#pragma warning restore CA1506
1250}
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
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
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.
async ValueTask CopyTo(string path, CancellationToken cancellationToken)
Copies the current working directory to a given path .A ValueTask representing the running operation.
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
Task PushHeadToTemporaryBranch(string username, string password, JobProgressReporter progressReporter, CancellationToken cancellationToken)
Force push the current repository HEAD to RemoteTemporaryBranchName;.
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.
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,...
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...
Repository(LibGit2Sharp.IRepository libGitRepo, ILibGit2Commands commands, IIOManager ioManager, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, IPostWriteHandler postWriteHandler, IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, ILibGit2RepositoryFactory submoduleFactory, ILogger< Repository > logger, GeneralConfiguration generalConfiguration, Action disposeAction)
Initializes a new instance of the Repository class.
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...
PushOptions IEnumerable< JobProgressReporter > SubProgressReporters GeneratePushOptions(JobProgressReporter progressReporter, string username, string password, CancellationToken cancellationToken)
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the Repository.
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 deploymentPipeline, CancellationToken cancellationToken)
Handle a given eventType .
CredentialsHandler GenerateCredentialsHandler(string? username, string? password)
Generate a CredentialsHandler from a given username and password .
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 .
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:13
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