tgstation-server 5.12.7
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;
10using Microsoft.Extensions.Logging;
11
19
21{
23 sealed class Repository : IRepository
24 {
28 public const string DefaultCommitterName = "tgstation-server";
29
33 public const string DefaultCommitterEmail = "tgstation-server@users.noreply.github.com";
34
38 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.";
39
43 public const string RemoteTemporaryBranchName = "___TGSTempBranch";
44
48 const string UnknownReference = "<UNKNOWN>";
49
52
55
58
60 public bool Tracking => Reference != null && libGitRepo.Head.IsTracking;
61
63 public string Head => libGitRepo.Head.Tip.Sha;
64
66 public string Reference => libGitRepo.Head.FriendlyName;
67
69 public Uri Origin => new (libGitRepo.Network.Remotes.First().Url);
70
74 readonly LibGit2Sharp.IRepository libGitRepo;
75
80
85
90
95
100
105
109 readonly ILogger<Repository> logger;
110
115
119 readonly Action onDispose;
120
125
140 LibGit2Sharp.IRepository libGitRepo,
146 IGitRemoteFeaturesFactory gitRemoteFeaturesFactory,
147 ILogger<Repository> logger,
149 Action onDispose)
150 {
151 this.libGitRepo = libGitRepo ?? throw new ArgumentNullException(nameof(libGitRepo));
152 this.commands = commands ?? throw new ArgumentNullException(nameof(commands));
153 this.ioMananger = ioMananger ?? throw new ArgumentNullException(nameof(ioMananger));
154 this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
155 this.credentialsProvider = credentialsProvider ?? throw new ArgumentNullException(nameof(credentialsProvider));
156 this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
157 ArgumentNullException.ThrowIfNull(gitRemoteFeaturesFactory);
158
159 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
160 this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration));
161 this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose));
162
163 gitRemoteFeatures = gitRemoteFeaturesFactory.CreateGitRemoteFeatures(this);
164 }
165
167 public void Dispose()
168 {
169 lock (onDispose)
170 {
171 if (disposed)
172 return;
173
174 disposed = true;
175 }
176
177 logger.LogTrace("Disposing...");
178 libGitRepo.Dispose();
179 onDispose();
180 }
181
183#pragma warning disable CA1506 // TODO: Decomplexify
184 public async Task<TestMergeResult> AddTestMerge(
185 TestMergeParameters testMergeParameters,
186 string committerName,
187 string committerEmail,
188 string username,
189 string password,
190 bool updateSubmodules,
191 JobProgressReporter progressReporter,
192 CancellationToken cancellationToken)
193 {
194 ArgumentNullException.ThrowIfNull(testMergeParameters);
195 ArgumentNullException.ThrowIfNull(committerName);
196 ArgumentNullException.ThrowIfNull(committerEmail);
197 ArgumentNullException.ThrowIfNull(progressReporter);
198
199 logger.LogDebug(
200 "Begin AddTestMerge: #{prNumber} at {targetSha} ({comment}) by <{committerName} ({committerEmail})>",
201 testMergeParameters.Number,
202 testMergeParameters.TargetCommitSha?[..7],
203 testMergeParameters.Comment,
204 committerName,
205 committerEmail);
206
207 if (RemoteGitProvider == Api.Models.RemoteGitProvider.Unknown)
208 throw new InvalidOperationException("Cannot test merge with an Unknown RemoteGitProvider!");
209
210 var commitMessage = String.Format(
211 CultureInfo.InvariantCulture,
212 "TGS Test Merge (#{0}){1}{2}",
213 testMergeParameters.Number,
214 testMergeParameters.Comment != null
215 ? Environment.NewLine
216 : String.Empty,
217 testMergeParameters.Comment ?? String.Empty);
218
219 var testMergeBranchName = String.Format(CultureInfo.InvariantCulture, "tm-{0}", testMergeParameters.Number);
220 var localBranchName = String.Format(CultureInfo.InvariantCulture, gitRemoteFeatures.TestMergeLocalBranchNameFormatter, testMergeParameters.Number, testMergeBranchName);
221
222 var refSpec = String.Format(CultureInfo.InvariantCulture, gitRemoteFeatures.TestMergeRefSpecFormatter, testMergeParameters.Number, testMergeBranchName);
223 var refSpecList = new List<string> { refSpec };
224 var logMessage = String.Format(CultureInfo.InvariantCulture, "Test merge #{0}", testMergeParameters.Number);
225
226 var originalCommit = libGitRepo.Head;
227
228 MergeResult result = null;
229
230 var progressFactor = 1.0 / (updateSubmodules ? 3 : 2);
231
232 var sig = new Signature(new Identity(committerName, committerEmail), DateTimeOffset.UtcNow);
233 List<string> conflictedPaths = null;
234 await Task.Factory.StartNew(
235 () =>
236 {
237 try
238 {
239 try
240 {
241 logger.LogTrace("Fetching refspec {refSpec}...", refSpec);
242
243 var remote = libGitRepo.Network.Remotes.First();
246 refSpecList,
247 remote,
248 new FetchOptions
249 {
250 Prune = true,
251 OnProgress = (a) => !cancellationToken.IsCancellationRequested,
252 OnTransferProgress = TransferProgressHandler(
253 progressReporter.CreateSection($"Fetch {refSpec}", progressFactor),
254 cancellationToken),
255 OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
256 CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password),
257 },
258 logMessage);
259 }
260 catch (UserCancelledException)
261 {
262 }
263 catch (LibGit2SharpException ex)
264 {
266 }
267
268 cancellationToken.ThrowIfCancellationRequested();
269
270 libGitRepo.RemoveUntrackedFiles();
271
272 cancellationToken.ThrowIfCancellationRequested();
273
274 var objectName = testMergeParameters.TargetCommitSha ?? localBranchName;
275 var gitObject = libGitRepo.Lookup(objectName) ?? throw new JobException($"Could not find object to merge: {objectName}");
276
277 testMergeParameters.TargetCommitSha = gitObject.Sha;
278
279 cancellationToken.ThrowIfCancellationRequested();
280
281 logger.LogTrace("Merging {targetCommitSha} into {currentReference}...", testMergeParameters.TargetCommitSha[..7], Reference);
282
283 result = libGitRepo.Merge(testMergeParameters.TargetCommitSha, sig, new MergeOptions
284 {
285 CommitOnSuccess = commitMessage == null,
286 FailOnConflict = false, // Needed to get conflicting files
287 FastForwardStrategy = FastForwardStrategy.NoFastForward,
288 SkipReuc = true,
289 OnCheckoutProgress = CheckoutProgressHandler(
290 progressReporter.CreateSection($"Merge {testMergeParameters.TargetCommitSha[..7]}", progressFactor)),
291 });
292 }
293 finally
294 {
295 libGitRepo.Branches.Remove(localBranchName);
296 }
297
298 cancellationToken.ThrowIfCancellationRequested();
299
300 if (result.Status == MergeStatus.Conflicts)
301 {
302 var repoStatus = libGitRepo.RetrieveStatus();
303 conflictedPaths = new List<string>();
304 foreach (var file in repoStatus)
305 if (file.State == FileStatus.Conflicted)
306 conflictedPaths.Add(file.FilePath);
307
308 var revertTo = originalCommit.CanonicalName ?? originalCommit.Tip.Sha;
309 logger.LogDebug("Merge conflict, aborting and reverting to {revertTarget}", revertTo);
310 progressReporter.ReportProgress(0);
311 RawCheckout(revertTo, progressReporter.CreateSection("Hard Reset to {revertTo}", 1.0), cancellationToken);
312 cancellationToken.ThrowIfCancellationRequested();
313 }
314
315 libGitRepo.RemoveUntrackedFiles();
316 },
317 cancellationToken,
319 TaskScheduler.Current);
320
321 if (result.Status == MergeStatus.Conflicts)
322 {
323 var arguments = new List<string>
324 {
325 originalCommit.Tip.Sha,
326 testMergeParameters.TargetCommitSha,
327 originalCommit.FriendlyName ?? UnknownReference,
328 testMergeBranchName,
329 };
330
331 arguments.AddRange(conflictedPaths);
332
334 EventType.RepoMergeConflict,
335 arguments,
336 false,
337 cancellationToken);
338 return new TestMergeResult
339 {
340 Status = result.Status,
341 ConflictingFiles = conflictedPaths,
342 };
343 }
344
345 if (result.Status != MergeStatus.UpToDate)
346 {
347 logger.LogTrace("Committing merge: \"{commitMessage}\"...", commitMessage);
348 await Task.Factory.StartNew(
349 () => libGitRepo.Commit(commitMessage, sig, sig, new CommitOptions
350 {
351 PrettifyMessage = true,
352 }),
353 cancellationToken,
355 TaskScheduler.Current);
356
357 if (updateSubmodules)
358 {
359 await UpdateSubmodules(
360 progressReporter.CreateSection("Update Submodules", progressFactor),
361 username,
362 password,
363 false,
364 cancellationToken);
365 }
366 }
367
369 EventType.RepoAddTestMerge,
370 new List<string>
371 {
372 testMergeParameters.Number.ToString(CultureInfo.InvariantCulture),
373 testMergeParameters.TargetCommitSha,
374 testMergeParameters.Comment,
375 },
376 false,
377 cancellationToken);
378
379 return new TestMergeResult
380 {
381 Status = result.Status,
382 };
383 }
384#pragma warning restore CA1506
385
387 public async Task CheckoutObject(
388 string committish,
389 string username,
390 string password,
391 bool updateSubmodules,
392 JobProgressReporter progressReporter,
393 CancellationToken cancellationToken)
394 {
395 ArgumentNullException.ThrowIfNull(committish);
396 ArgumentNullException.ThrowIfNull(progressReporter);
397 logger.LogDebug("Checkout object: {committish}...", committish);
398 await eventConsumer.HandleEvent(EventType.RepoCheckout, new List<string> { committish }, false, cancellationToken);
399 await Task.Factory.StartNew(
400 () =>
401 {
402 libGitRepo.RemoveUntrackedFiles();
404 committish,
405 progressReporter.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0),
406 cancellationToken);
407 },
408 cancellationToken,
410 TaskScheduler.Current);
411
412 if (updateSubmodules)
413 await UpdateSubmodules(
414 progressReporter.CreateSection(null, 1.0 / 3),
415 username,
416 password,
417 false,
418 cancellationToken);
419 }
420
422 public async Task FetchOrigin(
423 JobProgressReporter progressReporter,
424 string username,
425 string password,
426 bool deploymentPipeline,
427 CancellationToken cancellationToken)
428 {
429 ArgumentNullException.ThrowIfNull(progressReporter);
430 logger.LogDebug("Fetch origin...");
431 await eventConsumer.HandleEvent(EventType.RepoFetch, Enumerable.Empty<string>(), deploymentPipeline, cancellationToken);
432 await Task.Factory.StartNew(
433 () =>
434 {
435 var remote = libGitRepo.Network.Remotes.First();
436 try
437 {
440 remote
441 .FetchRefSpecs
442 .Select(x => x.Specification),
443 remote,
444 new FetchOptions
445 {
446 Prune = true,
447 OnProgress = (a) => !cancellationToken.IsCancellationRequested,
448 OnTransferProgress = TransferProgressHandler(progressReporter.CreateSection("Fetch Origin", 1.0), cancellationToken),
449 OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
450 CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password),
451 },
452 "Fetch origin commits");
453 }
454 catch (UserCancelledException)
455 {
456 cancellationToken.ThrowIfCancellationRequested();
457 }
458 catch (LibGit2SharpException ex)
459 {
461 }
462 },
463 cancellationToken,
465 TaskScheduler.Current);
466 }
467
469 public async Task ResetToOrigin(
470 JobProgressReporter progressReporter,
471 string username,
472 string password,
473 bool updateSubmodules,
474 bool deploymentPipeline,
475 CancellationToken cancellationToken)
476 {
477 ArgumentNullException.ThrowIfNull(progressReporter);
478 if (!Tracking)
479 throw new JobException(ErrorCode.RepoReferenceRequired);
480 logger.LogTrace("Reset to origin...");
481 var trackedBranch = libGitRepo.Head.TrackedBranch;
482 await eventConsumer.HandleEvent(EventType.RepoResetOrigin, new List<string> { trackedBranch.FriendlyName, trackedBranch.Tip.Sha }, deploymentPipeline, cancellationToken);
483 await ResetToSha(
484 trackedBranch.Tip.Sha,
485 progressReporter.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0),
486 cancellationToken);
487
488 if (updateSubmodules)
489 await UpdateSubmodules(
490 progressReporter.CreateSection(null, 1.0 / 3),
491 username,
492 password,
493 deploymentPipeline,
494 cancellationToken);
495 }
496
498 public Task ResetToSha(string sha, JobProgressReporter progressReporter, CancellationToken cancellationToken) => Task.Factory.StartNew(
499 () =>
500 {
501 ArgumentNullException.ThrowIfNull(sha);
502 ArgumentNullException.ThrowIfNull(progressReporter);
503
504 logger.LogDebug("Reset to sha: {sha}", sha[..7]);
505
506 libGitRepo.RemoveUntrackedFiles();
507 cancellationToken.ThrowIfCancellationRequested();
508
509 var gitObject = libGitRepo.Lookup(sha, ObjectType.Commit);
510 cancellationToken.ThrowIfCancellationRequested();
511
512 if (gitObject == null)
513 throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Cannot reset to non-existent SHA: {0}", sha));
514
515 libGitRepo.Reset(ResetMode.Hard, gitObject.Peel<Commit>(), new CheckoutOptions
516 {
517 OnCheckoutProgress = CheckoutProgressHandler(progressReporter.CreateSection($"Reset to {gitObject.Sha}", 1.0)),
518 });
519 },
520 cancellationToken,
522 TaskScheduler.Current);
523
525 public async Task CopyTo(string path, CancellationToken cancellationToken)
526 {
527 ArgumentNullException.ThrowIfNull(path);
528 logger.LogTrace("Copying to {path}...", path);
530 new List<string> { ".git" },
531 (src, dest) =>
532 {
535
536 return Task.CompletedTask;
537 },
539 path,
540 generalConfiguration.GetCopyDirectoryTaskThrottle(),
541 cancellationToken);
542 }
543
545 public Task<string> GetOriginSha(CancellationToken cancellationToken) => Task.Factory.StartNew(
546 () =>
547 {
548 if (!Tracking)
549 throw new JobException(ErrorCode.RepoReferenceRequired);
550
551 cancellationToken.ThrowIfCancellationRequested();
552
553 return libGitRepo.Head.TrackedBranch.Tip.Sha;
554 },
555 cancellationToken,
557 TaskScheduler.Current);
558
560 public async Task<bool?> MergeOrigin(
561 JobProgressReporter progressReporter,
562 string committerName,
563 string committerEmail,
564 bool deploymentPipeline,
565 CancellationToken cancellationToken)
566 {
567 ArgumentNullException.ThrowIfNull(progressReporter);
568
569 MergeResult result = null;
570 Branch trackedBranch = null;
571
572 var oldHead = libGitRepo.Head;
573 var oldTip = oldHead.Tip;
574
575 await Task.Factory.StartNew(
576 () =>
577 {
578 if (!Tracking)
579 throw new JobException(ErrorCode.RepoReferenceRequired);
580
581 libGitRepo.RemoveUntrackedFiles();
582
583 cancellationToken.ThrowIfCancellationRequested();
584
585 trackedBranch = libGitRepo.Head.TrackedBranch;
586 logger.LogDebug(
587 "Merge origin/{trackedBranch}: <{committerName} ({committerEmail})>",
588 trackedBranch.FriendlyName,
589 committerName,
590 committerEmail);
591 result = libGitRepo.Merge(trackedBranch, new Signature(committerName, committerEmail, DateTimeOffset.UtcNow), new MergeOptions
592 {
593 CommitOnSuccess = true,
594 FailOnConflict = true,
595 FastForwardStrategy = FastForwardStrategy.Default,
596 SkipReuc = true,
597 OnCheckoutProgress = CheckoutProgressHandler(progressReporter.CreateSection("Merge Origin", 1.0)),
598 });
599
600 cancellationToken.ThrowIfCancellationRequested();
601
602 if (result.Status == MergeStatus.Conflicts)
603 {
604 logger.LogDebug("Merge conflict, aborting and reverting to {oldHeadFriendlyName}", oldHead.FriendlyName);
605 progressReporter.ReportProgress(0);
606 libGitRepo.Reset(ResetMode.Hard, oldTip, new CheckoutOptions
607 {
608 OnCheckoutProgress = CheckoutProgressHandler(progressReporter.CreateSection($"Hard Reset to {oldHead.FriendlyName}", 1.0)),
609 });
610 cancellationToken.ThrowIfCancellationRequested();
611 }
612
613 libGitRepo.RemoveUntrackedFiles();
614 },
615 cancellationToken,
617 TaskScheduler.Current);
618
619 if (result.Status == MergeStatus.Conflicts)
620 {
622 EventType.RepoMergeConflict,
623 new List<string>
624 {
625 oldTip.Sha,
626 trackedBranch.Tip.Sha,
627 oldHead.FriendlyName ?? UnknownReference,
628 trackedBranch.FriendlyName,
629 },
630 deploymentPipeline,
631 cancellationToken);
632 return null;
633 }
634
635 return result.Status == MergeStatus.FastForward;
636 }
637
639 public async Task<bool> Sychronize(
640 JobProgressReporter progressReporter,
641 string username,
642 string password,
643 string committerName,
644 string committerEmail,
645 bool synchronizeTrackedBranch,
646 bool deploymentPipeline,
647 CancellationToken cancellationToken)
648 {
649 ArgumentNullException.ThrowIfNull(committerName);
650 ArgumentNullException.ThrowIfNull(committerEmail);
651 ArgumentNullException.ThrowIfNull(progressReporter);
652
653 if (username == null && password == null)
654 {
655 logger.LogTrace("Not synchronizing due to lack of credentials!");
656 return false;
657 }
658
659 logger.LogTrace("Begin Synchronize...");
660
661 ArgumentNullException.ThrowIfNull(username);
662 ArgumentNullException.ThrowIfNull(password);
663
664 var startHead = Head;
665
666 logger.LogTrace("Configuring <{committerName} ({committerEmail})> as author/committer", committerName, committerEmail);
667 await Task.Factory.StartNew(
668 () =>
669 {
670 libGitRepo.Config.Set("user.name", committerName);
671 cancellationToken.ThrowIfCancellationRequested();
672 libGitRepo.Config.Set("user.email", committerEmail);
673 },
674 cancellationToken,
676 TaskScheduler.Current);
677
678 cancellationToken.ThrowIfCancellationRequested();
679 try
680 {
681 await eventConsumer.HandleEvent(
682 EventType.RepoPreSynchronize,
683 new List<string>
684 {
685 ioMananger.ResolvePath(),
686 },
687 deploymentPipeline,
688 cancellationToken);
689 }
690 finally
691 {
692 logger.LogTrace("Resetting and cleaning untracked files...");
693 await Task.Factory.StartNew(
694 () =>
695 {
696 libGitRepo.Reset(ResetMode.Hard, libGitRepo.Head.Tip, new CheckoutOptions
697 {
698 OnCheckoutProgress = CheckoutProgressHandler(progressReporter.CreateSection("Hard reset and remove untracked files", 0.1)),
699 });
700 cancellationToken.ThrowIfCancellationRequested();
701 libGitRepo.RemoveUntrackedFiles();
702 },
703 cancellationToken,
705 TaskScheduler.Current);
706 }
707
708 var remainingProgressFactor = 0.9;
709 if (!synchronizeTrackedBranch)
710 {
711 await PushHeadToTemporaryBranch(
712 username,
713 password,
714 progressReporter.CreateSection("Push to temporary branch", remainingProgressFactor),
715 cancellationToken);
716 return false;
717 }
718
719 var sameHead = Head == startHead;
720 if (sameHead || !Tracking)
721 {
722 logger.LogTrace("Aborted synchronize due to {abortReason}!", sameHead ? "lack of changes" : "not being on tracked reference");
723 return false;
724 }
725
726 logger.LogInformation("Synchronizing with origin...");
727
728 return await Task.Factory.StartNew(
729 () =>
730 {
731 var remote = libGitRepo.Network.Remotes.First();
732 try
733 {
734 libGitRepo.Network.Push(
735 libGitRepo.Head,
736 GeneratePushOptions(
737 progressReporter.CreateSection("Push to origin", remainingProgressFactor),
738 username,
739 password,
740 cancellationToken));
741 return true;
742 }
743 catch (NonFastForwardException)
744 {
745 logger.LogInformation("Synchronize aborted, non-fast forward!");
746 return false;
747 }
748 catch (UserCancelledException e)
749 {
750 cancellationToken.ThrowIfCancellationRequested();
751 throw new InvalidOperationException("Caught UserCancelledException without cancellationToken triggering", e);
752 }
753 catch (LibGit2SharpException e)
754 {
755 logger.LogWarning(e, "Unable to make synchronization push!");
756 return false;
757 }
758 },
759 cancellationToken,
761 TaskScheduler.Current);
762 }
763
765 public Task<bool> IsSha(string committish, CancellationToken cancellationToken) => Task.Factory.StartNew(
766 () =>
767 {
768 // check if it's a tag
769 var gitObject = libGitRepo.Lookup(committish, ObjectType.Tag);
770 if (gitObject != null)
771 return false;
772 cancellationToken.ThrowIfCancellationRequested();
773
774 // check if it's a branch
775 if (libGitRepo.Branches[committish] != null)
776 return false;
777 cancellationToken.ThrowIfCancellationRequested();
778
779 // err on the side of references, if we can't look it up, assume its a reference
780 if (libGitRepo.Lookup<Commit>(committish) != null)
781 return true;
782 return false;
783 },
784 cancellationToken,
786 TaskScheduler.Current);
787
789 public Task<bool> ShaIsParent(string sha, CancellationToken cancellationToken) => Task.Factory.StartNew(
790 () =>
791 {
792 var targetCommit = libGitRepo.Lookup<Commit>(sha);
793 if (targetCommit == null)
794 {
795 logger.LogTrace("Commit {sha} not found in repository", sha);
796 return false;
797 }
798
799 cancellationToken.ThrowIfCancellationRequested();
800 var startSha = Head;
801 var mergeResult = libGitRepo.Merge(
802 targetCommit,
803 new Signature(
804 DefaultCommitterName,
805 DefaultCommitterEmail,
806 DateTimeOffset.UtcNow),
807 new MergeOptions
808 {
809 FastForwardStrategy = FastForwardStrategy.FastForwardOnly,
810 FailOnConflict = true,
811 });
812
813 if (mergeResult.Status == MergeStatus.UpToDate)
814 return true;
815
816 commands.Checkout(
817 libGitRepo,
818 new CheckoutOptions
819 {
820 CheckoutModifiers = CheckoutModifiers.Force,
821 },
822 startSha);
823
824 return false;
825 },
826 cancellationToken,
828 TaskScheduler.Current);
829
831 public Task<Models.TestMerge> GetTestMerge(
832 TestMergeParameters parameters,
833 RepositorySettings repositorySettings,
834 CancellationToken cancellationToken) => gitRemoteFeatures.GetTestMerge(
835 parameters,
836 repositorySettings,
837 cancellationToken);
838
840 public Task<DateTimeOffset> TimestampCommit(string sha, CancellationToken cancellationToken) => Task.Factory.StartNew(
841 () =>
842 {
843 ArgumentNullException.ThrowIfNull(sha);
844
845 var commit = libGitRepo.Lookup<Commit>(sha) ?? throw new JobException($"Commit {sha} does not exist in the repository!");
846 return commit.Committer.When.ToUniversalTime();
847 },
848 cancellationToken,
850 TaskScheduler.Current);
851
858 void RawCheckout(string committish, JobProgressReporter progressReporter, CancellationToken cancellationToken)
859 {
860 logger.LogTrace("Checkout: {committish}", committish);
861
862 var stage = $"Checkout {committish}";
863 progressReporter = progressReporter.CreateSection(stage, 1.0);
864 progressReporter.ReportProgress(0);
865 cancellationToken.ThrowIfCancellationRequested();
866
867 var checkoutOptions = new CheckoutOptions
868 {
869 CheckoutModifiers = CheckoutModifiers.Force,
870 OnCheckoutProgress = CheckoutProgressHandler(progressReporter),
871 };
872
873 void RunCheckout() => commands.Checkout(
874 libGitRepo,
875 checkoutOptions,
876 committish);
877
878 try
879 {
880 RunCheckout();
881 }
882 catch (NotFoundException)
883 {
884 // Maybe (likely) a remote?
885 var remoteName = $"origin/{committish}";
886 var remoteBranch = libGitRepo.Branches.FirstOrDefault(
887 branch => branch.FriendlyName.Equals(remoteName, StringComparison.Ordinal));
888 cancellationToken.ThrowIfCancellationRequested();
889
890 if (remoteBranch == default)
891 throw;
892
893 logger.LogDebug("Creating local branch for {remoteBranchFriendlyName}...", remoteBranch.FriendlyName);
894 var branch = libGitRepo.CreateBranch(committish, remoteBranch.Tip);
895
896 libGitRepo.Branches.Update(branch, branchUpdate => branchUpdate.TrackedBranch = remoteBranch.CanonicalName);
897
898 cancellationToken.ThrowIfCancellationRequested();
899
900 RunCheckout();
901 }
902
903 cancellationToken.ThrowIfCancellationRequested();
904
905 libGitRepo.RemoveUntrackedFiles();
906 }
907
916 Task PushHeadToTemporaryBranch(string username, string password, JobProgressReporter progressReporter, CancellationToken cancellationToken) => Task.Factory.StartNew(
917 () =>
918 {
919 logger.LogInformation("Pushing changes to temporary remote branch...");
920 var branch = libGitRepo.CreateBranch(RemoteTemporaryBranchName);
921 try
922 {
923 cancellationToken.ThrowIfCancellationRequested();
924 var remote = libGitRepo.Network.Remotes.First();
925 try
926 {
927 var forcePushString = String.Format(CultureInfo.InvariantCulture, "+{0}:{0}", branch.CanonicalName);
928 libGitRepo.Network.Push(remote, forcePushString, GeneratePushOptions(progressReporter.CreateSection(null, 0.9), username, password, cancellationToken));
929 var removalString = String.Format(CultureInfo.InvariantCulture, ":{0}", branch.CanonicalName);
930 libGitRepo.Network.Push(remote, removalString, GeneratePushOptions(progressReporter.CreateSection(null, 0.1), username, password, cancellationToken));
931 }
932 catch (UserCancelledException)
933 {
934 cancellationToken.ThrowIfCancellationRequested();
935 }
936 catch (LibGit2SharpException e)
937 {
938 logger.LogWarning(e, "Unable to push to temporary branch!");
939 }
940 }
941 finally
942 {
943 libGitRepo.Branches.Remove(branch);
944 }
945 },
946 cancellationToken,
948 TaskScheduler.Current);
949
958 PushOptions GeneratePushOptions(JobProgressReporter progressReporter, string username, string password, CancellationToken cancellationToken)
959 {
960 var subProgressReporter = progressReporter.CreateSection(null, 0.5);
961
962 return new PushOptions
963 {
964 OnPackBuilderProgress = (stage, current, total) =>
965 {
966 var baseProgress = stage == PackBuilderStage.Counting ? 0 : 0.5;
967 var addon = total > 0 && current <= total ? (0.5 * ((double)current / total)) : 0;
968 progressReporter.ReportProgress(baseProgress + addon);
969 return !cancellationToken.IsCancellationRequested;
970 },
971 OnNegotiationCompletedBeforePush = (a) =>
972 {
973 subProgressReporter = progressReporter.CreateSection(null, 0.5);
974 return !cancellationToken.IsCancellationRequested;
975 },
976 OnPushTransferProgress = (a, sentBytes, totalBytes) =>
977 {
978 progressReporter.ReportProgress((double)sentBytes / totalBytes);
979 return !cancellationToken.IsCancellationRequested;
980 },
981 CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password),
982 };
983 }
984
995 JobProgressReporter progressReporter,
996 string username,
997 string password,
998 bool deploymentPipeline,
999 CancellationToken cancellationToken)
1000 {
1001 var submoduleCount = libGitRepo.Submodules.Count();
1002 if (submoduleCount == 0)
1003 {
1004 logger.LogTrace("No submodules, skipping update");
1005 return;
1006 }
1007
1008 logger.LogTrace("Updating submodules with{orWithout} credentials...", username == null ? "out" : String.Empty);
1009
1010 var factor = 1.0 / submoduleCount / 2;
1011 foreach (var submodule in libGitRepo.Submodules)
1012 {
1013 var submoduleUpdateOptions = new SubmoduleUpdateOptions
1014 {
1015 Init = true,
1016 OnTransferProgress = TransferProgressHandler(
1017 progressReporter.CreateSection($"Fetch submodule {submodule.Name}", factor),
1018 cancellationToken),
1019 OnProgress = output => !cancellationToken.IsCancellationRequested,
1020 OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
1021 CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password),
1022 OnCheckoutProgress = CheckoutProgressHandler(
1023 progressReporter.CreateSection($"Checkout submodule {submodule.Name}", factor)),
1024 };
1025
1026 logger.LogDebug("Updating submodule {submoduleName}...", submodule.Name);
1027 Task RawSubModuleUpdate() => Task.Factory.StartNew(
1028 () => libGitRepo.Submodules.Update(submodule.Name, submoduleUpdateOptions),
1029 cancellationToken,
1031 TaskScheduler.Current);
1032 try
1033 {
1034 await RawSubModuleUpdate();
1035 }
1036 catch (LibGit2SharpException ex)
1037 {
1038 // workaround for https://github.com/libgit2/libgit2/issues/3820
1039 // kill off the modules/ folder in .git and try again
1040 progressReporter.ReportProgress(null);
1041 credentialsProvider.CheckBadCredentialsException(ex);
1042 logger.LogWarning(ex, "Initial update of submodule {submoduleName} failed. Deleting submodule directories and re-attempting...", submodule.Name);
1043
1044 await Task.WhenAll(
1045 ioMananger.DeleteDirectory($".git/modules/{submodule.Path}", cancellationToken),
1046 ioMananger.DeleteDirectory(submodule.Path, cancellationToken));
1047
1048 logger.LogTrace("Second update attempt for submodule {submoduleName}...", submodule.Name);
1049 try
1050 {
1051 await RawSubModuleUpdate();
1052 }
1053 catch (UserCancelledException)
1054 {
1055 cancellationToken.ThrowIfCancellationRequested();
1056 }
1057 catch (LibGit2SharpException ex2)
1058 {
1059 credentialsProvider.CheckBadCredentialsException(ex2);
1060 logger.LogTrace(ex2, "Retried update of submodule {submoduleName} failed!", submodule.Name);
1061 throw new AggregateException(ex, ex2);
1062 }
1063 }
1064
1065 await eventConsumer.HandleEvent(
1066 EventType.RepoSubmoduleUpdate,
1067 new List<string> { submodule.Name },
1068 deploymentPipeline,
1069 cancellationToken);
1070 }
1071 }
1072
1078 CheckoutProgressHandler CheckoutProgressHandler(JobProgressReporter progressReporter) => (a, completedSteps, totalSteps) =>
1079 {
1080 double? percentage;
1081
1082 // short circuit initialization where totalSteps is 0
1083 if (completedSteps == 0)
1084 percentage = 0;
1085 else if (totalSteps < completedSteps || totalSteps == 0)
1086 percentage = null;
1087 else
1088 {
1089 percentage = ((double)completedSteps) / totalSteps;
1090 if (percentage < 0)
1091 percentage = null;
1092 }
1093
1094 if (percentage == null)
1095 logger.LogDebug(
1096 "Bad checkout progress values (Please tell Dominion)! Completeds: {completed}, Total: {total}",
1097 completedSteps,
1098 totalSteps);
1099
1100 progressReporter.ReportProgress(percentage);
1101 };
1102
1109 TransferProgressHandler TransferProgressHandler(JobProgressReporter progressReporter, CancellationToken cancellationToken) => (transferProgress) =>
1110 {
1111 double? percentage;
1112 var totalObjectsToProcess = transferProgress.TotalObjects * 2;
1113 var processedObjects = transferProgress.IndexedObjects + transferProgress.ReceivedObjects;
1114 if (totalObjectsToProcess < processedObjects || totalObjectsToProcess == 0)
1115 percentage = null;
1116 else
1117 {
1118 percentage = (double)processedObjects / totalObjectsToProcess;
1119 if (percentage < 0)
1120 percentage = null;
1121 }
1122
1123 if (percentage == null)
1124 logger.LogDebug(
1125 "Bad transfer progress values (Please tell Cyberboss)! Indexed: {indexed}, Received: {received}, Total: {total}",
1126 transferProgress.IndexedObjects,
1127 transferProgress.ReceivedObjects,
1128 transferProgress.TotalObjects);
1129
1130 progressReporter.ReportProgress(percentage);
1131 return !cancellationToken.IsCancellationRequested;
1132 };
1133 }
1134}
Represents configurable settings for a git repository.
Parameters for creating a TestMerge.
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:66
readonly IIOManager ioMananger
The IIOManager for the Repository.
Definition: Repository.cs:84
void RawCheckout(string committish, JobProgressReporter progressReporter, CancellationToken cancellationToken)
Runs a blocking force checkout to committish .
Definition: Repository.cs:858
readonly ILibGit2Commands commands
The ILibGit2Commands for the Repository.
Definition: Repository.cs:79
async Task CopyTo(string path, CancellationToken cancellationToken)
Copies the current working directory to a given path . A Task representing the running operation.
Definition: Repository.cs:525
const string OriginTrackingErrorTemplate
Template error message for when tracking of the most recent origin commit fails.
Definition: Repository.cs:38
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:33
string Head
The SHA of the IRepository HEAD.
Definition: Repository.cs:63
bool Tracking
If Reference tracks an upstream branch.
Definition: Repository.cs:60
const string RemoteTemporaryBranchName
The branch name used for publishing testmerge commits.
Definition: Repository.cs:43
string RemoteRepositoryOwner
If RemoteGitProvider is not RemoteGitProvider.Unknown this will be set with the owner of the reposito...
Definition: Repository.cs:54
bool disposed
If the Repository was disposed.
Definition: Repository.cs:124
Uri Origin
The current origin remote the IRepository is using.
Definition: Repository.cs:69
Repository(LibGit2Sharp.IRepository libGitRepo, ILibGit2Commands commands, IIOManager ioMananger, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, IPostWriteHandler postWriteHandler, IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, ILogger< Repository > logger, GeneralConfiguration generalConfiguration, Action onDispose)
Initializes a new instance of the Repository class.
Definition: Repository.cs:139
Task< Models.TestMerge > GetTestMerge(TestMergeParameters parameters, RepositorySettings repositorySettings, CancellationToken cancellationToken)
Retrieve the Models.TestMerge representation of given test merge parameters . A Task<TResult> resulti...
readonly LibGit2Sharp.IRepository libGitRepo
The LibGit2Sharp.IRepository for the Repository.
Definition: Repository.cs:74
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.
Definition: Repository.cs:104
async Task FetchOrigin(JobProgressReporter progressReporter, string username, string password, bool deploymentPipeline, CancellationToken cancellationToken)
Fetch commits from the origin repository. A Task representing the running operation.
Definition: Repository.cs:422
TransferProgressHandler TransferProgressHandler(JobProgressReporter progressReporter, CancellationToken cancellationToken)
Generate a LibGit2Sharp.Handlers.TransferProgressHandler from a given progressReporter and cancellat...
PushOptions GeneratePushOptions(JobProgressReporter progressReporter, string username, string password, CancellationToken cancellationToken)
Generate a standard set of PushOptions.
Definition: Repository.cs:958
readonly ILogger< Repository > logger
The ILogger for the Repository.
Definition: Repository.cs:109
async Task< bool > Sychronize(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...
Definition: Repository.cs:639
async Task< 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...
Definition: Repository.cs:560
async Task 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...
Definition: Repository.cs:469
Task< bool > ShaIsParent(string sha, CancellationToken cancellationToken)
Check if a given sha is a parent of the current Head. A Task<TResult> resulting in true if sha is a...
readonly IPostWriteHandler postWriteHandler
The IPostWriteHandler for the Repository.
Definition: Repository.cs:99
const string DefaultCommitterName
The default username for committers.
Definition: Repository.cs:28
const string UnknownReference
Used when a reference cannot be determined.
Definition: Repository.cs:48
Task< string > GetOriginSha(CancellationToken cancellationToken)
Get the tracked reference's current SHA. A Task<TResult> resulting in the tracked origin reference'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,...
async Task UpdateSubmodules(JobProgressReporter progressReporter, string username, string password, bool deploymentPipeline, CancellationToken cancellationToken)
Recusively update all Submodules in the libGitRepo.
Definition: Repository.cs:994
readonly IEventConsumer eventConsumer
The IEventConsumer for the Repository.
Definition: Repository.cs:89
readonly Action onDispose
Action to be taken when Dispose is called.
Definition: Repository.cs:119
async Task CheckoutObject(string committish, string username, string password, bool updateSubmodules, JobProgressReporter progressReporter, CancellationToken cancellationToken)
Checks out a given committish . A Task representing the running operation.
Definition: Repository.cs:387
CheckoutProgressHandler CheckoutProgressHandler(JobProgressReporter progressReporter)
Converts a given progressReporter to a LibGit2Sharp.Handlers.CheckoutProgressHandler.
async Task< 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....
Definition: Repository.cs:184
string RemoteRepositoryName
If RemoteGitProvider is not RemoteGitProvider.Unknown this will be set with the name of the repositor...
Definition: Repository.cs:57
readonly ICredentialsProvider credentialsProvider
The ICredentialsProvider for the Repository.
Definition: Repository.cs:94
Task< DateTimeOffset > TimestampCommit(string sha, CancellationToken cancellationToken)
Gets the DateTimeOffset a given sha was created on. A Task<TResult> resulting in the DateTimeOffset ...
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the Repository.
Definition: Repository.cs:114
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.
Definition: JobException.cs:11
void ReportProgress(double? progress)
Report progress.
JobProgressReporter CreateSection(string newStageName, double percentage)
Create a subsection of the JobProgressReporter with its optional own stage name.
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.
Task 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.
Definition: IRepository.cs:14
string Head
The SHA of the IRepository HEAD.
Definition: IRepository.cs:23
Interface for using filesystems.
Definition: IIOManager.cs:13
string ResolvePath()
Retrieve the full path of the current working directory.
Task CopyDirectory(IEnumerable< string > ignore, Func< string, string, Task > 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:11
RemoteGitProvider
Indicates the remote git host.
EventType
Types of events. Mirror in tgs.dm.
Definition: EventType.cs:7