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