public async Task <IActionResult> Delete(CancellationToken cancellationToken) { var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (currentModel == default) { return(StatusCode((int)HttpStatusCode.Gone)); } currentModel.AccessToken = null; currentModel.AccessUser = null; await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); Logger.LogInformation("Instance {0} repository delete initiated by user {1}", Instance.Id, AuthenticationContext.User.Id); var job = new Models.Job { Description = "Delete repository", StartedBy = AuthenticationContext.User, Instance = Instance }; var api = currentModel.ToApi(); await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressReporter, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false); api.ActiveJob = job.ToApi(); return(Accepted(api)); }
public override async Task <IActionResult> Create([FromBody] DreamDaemon model, CancellationToken cancellationToken) { //alias for launching DD var instance = instanceManager.GetInstance(Instance); if (instance.Watchdog.Running) { return(StatusCode((int)HttpStatusCode.Gone)); } var job = new Models.Job { Description = "Launch DreamDaemon", CancelRight = (ulong)DreamDaemonRights.Shutdown, CancelRightsType = RightsType.DreamDaemon, Instance = Instance, StartedBy = AuthenticationContext.User }; await jobManager.RegisterOperation(job, async (paramJob, serviceProvider, progressHandler, innerCt) => { var result = await instance.Watchdog.Launch(innerCt).ConfigureAwait(false); if (result == null) { throw new InvalidOperationException("Watchdog already running!"); } }, cancellationToken).ConfigureAwait(false); return(Accepted(job.ToApi())); }
public override async Task <IActionResult> Create([FromBody] DreamMaker model, CancellationToken cancellationToken) { var job = new Models.Job { Description = "Compile active repository code", StartedBy = AuthenticationContext.User, CancelRightsType = RightsType.DreamMaker, CancelRight = (ulong)DreamMakerRights.CancelCompile, Instance = Instance }; await jobManager.RegisterOperation(job, instanceManager.GetInstance(Instance).CompileProcess, cancellationToken).ConfigureAwait(false); return(Accepted(job.ToApi())); }
public override async Task <IActionResult> Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (model.Version == null) { return(BadRequest(new ErrorMessage { Message = "Missing version!" })); } var byondManager = instanceManager.GetInstance(Instance).ByondManager; // remove cruff fields var installingVersion = new Version(model.Version.Major, model.Version.Minor); var result = new Api.Models.Byond(); if (byondManager.InstalledVersions.Any(x => x == model.Version)) { Logger.LogInformation("User ID {0} changing instance ID {1} BYOND version to {2}", AuthenticationContext.User.Id, Instance.Id, installingVersion); await byondManager.ChangeVersion(model.Version, cancellationToken).ConfigureAwait(false); } else { Logger.LogInformation("User ID {0} installing BYOND version to {2} on instance ID {1}", AuthenticationContext.User.Id, Instance.Id, installingVersion); // run the install through the job manager var job = new Models.Job { Description = String.Format(CultureInfo.InvariantCulture, "Install BYOND version {0}", installingVersion), StartedBy = AuthenticationContext.User, CancelRightsType = RightsType.Byond, CancelRight = (ulong)ByondRights.CancelInstall, Instance = Instance }; await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, ct) => byondManager.ChangeVersion(installingVersion, ct), cancellationToken).ConfigureAwait(false); result.InstallJob = job.ToApi(); } if ((AuthenticationContext.GetRight(RightsType.Byond) & (ulong)ByondRights.ReadActive) != 0) { result.Version = byondManager.ActiveVersion; } return(result.InstallJob != null ? (IActionResult)Accepted(result) : Json(result)); }
public async Task <IActionResult> Restart(CancellationToken cancellationToken) { var job = new Models.Job { Instance = Instance, CancelRightsType = RightsType.DreamDaemon, CancelRight = (ulong)DreamDaemonRights.Shutdown, StartedBy = AuthenticationContext.User, Description = "Restart Watchdog" }; var watchdog = instanceManager.GetInstance(Instance).Watchdog; await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressReporter, ct) => watchdog.Restart(false, ct), cancellationToken).ConfigureAwait(false); return(Accepted(job.ToApi())); }
public async Task <IActionResult> Create(CancellationToken cancellationToken) { var job = new Models.Job { Description = "Compile active repository code", StartedBy = AuthenticationContext.User, CancelRightsType = RightsType.DreamMaker, CancelRight = (ulong)DreamMakerRights.CancelCompile, Instance = Instance }; await jobManager.RegisterOperation( job, (core, databaseContextFactory, paramJob, progressReporter, jobCancellationToken) => core.DreamMaker.DeploymentProcess(paramJob, databaseContextFactory, progressReporter, jobCancellationToken), cancellationToken) .ConfigureAwait(false); return(Accepted(job.ToApi())); }
public async Task <IActionResult> Create(CancellationToken cancellationToken) { // alias for launching DD var instance = instanceManager.GetInstance(Instance); if (instance.Watchdog.Running) { return(StatusCode((int)HttpStatusCode.Gone)); } var job = new Models.Job { Description = "Launch DreamDaemon", CancelRight = (ulong)DreamDaemonRights.Shutdown, CancelRightsType = RightsType.DreamDaemon, Instance = Instance, StartedBy = AuthenticationContext.User }; await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, innerCt) => instance.Watchdog.Launch(innerCt), cancellationToken).ConfigureAwait(false); return(Accepted(job.ToApi())); }
public Task <IActionResult> Create(CancellationToken cancellationToken) => WithComponentInstance(async instance => { if (instance.Watchdog.Status != WatchdogStatus.Offline) { return(Conflict(new ErrorMessage(ErrorCode.WatchdogRunning))); } var job = new Models.Job { Description = "Launch DreamDaemon", CancelRight = (ulong)DreamDaemonRights.Shutdown, CancelRightsType = RightsType.DreamDaemon, Instance = Instance, StartedBy = AuthenticationContext.User }; await jobManager.RegisterOperation( job, (core, databaseContextFactory, paramJob, progressHandler, innerCt) => core.Watchdog.Launch(innerCt), cancellationToken) .ConfigureAwait(false); return(Accepted(job.ToApi())); });
#pragma warning disable CA1506 // TODO: Decomplexify public async Task <IActionResult> Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken) #pragma warning restore CA1506 { if (model == null) { throw new ArgumentNullException(nameof(model)); } var uploadingZip = model.UploadCustomZip == true; if (model.Version == null || model.Version.Revision != -1 || (uploadingZip && model.Version.Build > 0)) { return(BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure))); } var userByondRights = AuthenticationContext.InstancePermissionSet.ByondRights.Value; if ((!userByondRights.HasFlag(ByondRights.InstallOfficialOrChangeActiveVersion) && !uploadingZip) || (!userByondRights.HasFlag(ByondRights.InstallCustomVersion) && uploadingZip)) { return(Forbid()); } // remove cruff fields var result = new Api.Models.Byond(); return(await WithComponentInstance( async instance => { var byondManager = instance.ByondManager; if (!uploadingZip && byondManager.InstalledVersions.Any(x => x == model.Version)) { Logger.LogInformation( "User ID {0} changing instance ID {1} BYOND version to {2}", AuthenticationContext.User.Id, Instance.Id, model.Version); await byondManager.ChangeVersion(model.Version, null, cancellationToken).ConfigureAwait(false); } else if (model.Version.Build > 0) { return BadRequest(new ErrorMessage(ErrorCode.ByondNonExistentCustomVersion)); } else { var installingVersion = model.Version.Build <= 0 ? new Version(model.Version.Major, model.Version.Minor) : model.Version; Logger.LogInformation( "User ID {0} installing BYOND version to {1} on instance ID {2}", AuthenticationContext.User.Id, installingVersion, Instance.Id); // run the install through the job manager var job = new Models.Job { Description = $"Install {(!uploadingZip ? String.Empty : "custom ")}BYOND version {model.Version.Major}.{model.Version.Minor}", StartedBy = AuthenticationContext.User, CancelRightsType = RightsType.Byond, CancelRight = (ulong)ByondRights.CancelInstall, Instance = Instance }; IFileUploadTicket fileUploadTicket = null; if (uploadingZip) { fileUploadTicket = fileTransferService.CreateUpload(false); } try { await jobManager.RegisterOperation( job, async(core, databaseContextFactory, paramJob, progressHandler, jobCancellationToken) => { Stream zipFileStream = null; if (fileUploadTicket != null) { using (fileUploadTicket) { var uploadStream = await fileUploadTicket.GetResult(jobCancellationToken).ConfigureAwait(false); if (uploadStream == null) { throw new JobException(ErrorCode.FileUploadExpired); } zipFileStream = new MemoryStream(); try { await uploadStream.CopyToAsync(zipFileStream, jobCancellationToken).ConfigureAwait(false); } catch { await zipFileStream.DisposeAsync().ConfigureAwait(false); throw; } } } using (zipFileStream) await core.ByondManager.ChangeVersion( model.Version, zipFileStream, jobCancellationToken) .ConfigureAwait(false); }, cancellationToken) .ConfigureAwait(false); result.InstallJob = job.ToApi(); result.FileTicket = fileUploadTicket?.Ticket.FileTicket; } catch { fileUploadTicket?.Dispose(); throw; } } if ((AuthenticationContext.GetRight(RightsType.Byond) & (ulong)ByondRights.ReadActive) != 0) { result.Version = byondManager.ActiveVersion; } return result.InstallJob != null ? (IActionResult)Accepted(result) : Json(result); }) .ConfigureAwait(false)); }
#pragma warning disable CA1502 // TODO: Decomplexify #pragma warning disable CA1505 public async Task <IActionResult> Update([FromBody] Repository model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (model.AccessUser == null ^ model.AccessToken == null) { return(BadRequest(new ErrorMessage { Message = "Either both accessToken and accessUser must be present or neither!" })); } if (model.CheckoutSha != null && model.Reference != null) { return(BadRequest(new ErrorMessage { Message = "Only one of sha or reference may be specified!" })); } if (model.CheckoutSha != null && model.UpdateFromOrigin == true) { return(BadRequest(new ErrorMessage { Message = "Cannot update a reference when checking out a sha!" })); } if (model.Origin != null) { return(BadRequest(new ErrorMessage { Message = "origin cannot be modified without deleting the repository!" })); } if (model.NewTestMerges?.Any(x => !x.Number.HasValue) == true) { return(BadRequest(new ErrorMessage { Message = "All new test merges must provide a number!" })); } if (model.NewTestMerges?.Any(x => model.NewTestMerges.Any(y => x != y && x.Number == y.Number)) == true) { return(BadRequest(new ErrorMessage { Message = "Cannot test merge the same PR twice in one job!" })); } if (model.CommitterName?.Length == 0) { return(BadRequest(new ErrorMessage { Message = "Cannot set empty committer name!" })); } if (model.CommitterEmail?.Length == 0) { return(BadRequest(new ErrorMessage { Message = "Cannot set empty committer e=mail!" })); } var newTestMerges = model.NewTestMerges != null && model.NewTestMerges.Count > 0; var userRights = (RepositoryRights)AuthenticationContext.GetRight(RightsType.Repository); if (newTestMerges && !userRights.HasFlag(RepositoryRights.MergePullRequest)) { return(Forbid()); } var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (currentModel == default) { return(StatusCode((int)HttpStatusCode.Gone)); } bool CheckModified <T>(Expression <Func <Api.Models.Internal.RepositorySettings, T> > expression, RepositoryRights requiredRight) { var memberSelectorExpression = (MemberExpression)expression.Body; var property = (PropertyInfo)memberSelectorExpression.Member; var newVal = property.GetValue(model); if (newVal == null) { return(false); } if (!userRights.HasFlag(requiredRight) && property.GetValue(currentModel) != newVal) { return(true); } property.SetValue(currentModel, newVal); return(false); } if (CheckModified(x => x.AccessToken, RepositoryRights.ChangeCredentials) || CheckModified(x => x.AccessUser, RepositoryRights.ChangeCredentials) || CheckModified(x => x.AutoUpdatesKeepTestMerges, RepositoryRights.ChangeAutoUpdateSettings) || CheckModified(x => x.AutoUpdatesSynchronize, RepositoryRights.ChangeAutoUpdateSettings) || CheckModified(x => x.CommitterEmail, RepositoryRights.ChangeCommitter) || CheckModified(x => x.CommitterName, RepositoryRights.ChangeCommitter) || CheckModified(x => x.PushTestMergeCommits, RepositoryRights.ChangeTestMergeCommits) || CheckModified(x => x.ShowTestMergeCommitters, RepositoryRights.ChangeTestMergeCommits) || CheckModified(x => x.PostTestMergeComment, RepositoryRights.ChangeTestMergeCommits) || (model.UpdateFromOrigin == true && !userRights.HasFlag(RepositoryRights.UpdateBranch))) { return(Forbid()); } if (currentModel.AccessToken?.Length == 0 && currentModel.AccessUser?.Length == 0) { // setting an empty string clears everything currentModel.AccessUser = null; currentModel.AccessToken = null; } var canRead = userRights.HasFlag(RepositoryRights.Read); var api = canRead ? currentModel.ToApi() : new Repository(); var repoManager = instanceManager.GetInstance(Instance).RepositoryManager; if (canRead) { if (repoManager.CloneInProgress) { return(Conflict(new ErrorMessage { Message = "A clone operation is in progress!" })); } if (repoManager.InUse) { return(Conflict(new ErrorMessage { Message = "The repo is busy!" })); } using (var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false)) { if (repo == null) { return(Conflict(new ErrorMessage { Message = "Repository could not be loaded!" })); } await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false); } } // this is just db stuf so stow it away await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); // format the job description string description = null; if (model.UpdateFromOrigin == true) { if (model.Reference != null) { description = String.Format(CultureInfo.InvariantCulture, "Fetch and hard reset repository to origin/{0}", model.Reference); } else if (model.CheckoutSha != null) { description = String.Format(CultureInfo.InvariantCulture, "Fetch and checkout {0} in repository", model.CheckoutSha); } else { description = "Pull current repository reference"; } } else if (model.Reference != null || model.CheckoutSha != null) { description = String.Format(CultureInfo.InvariantCulture, "Checkout repository {0} {1}", model.Reference != null ? "reference" : "SHA", model.Reference ?? model.CheckoutSha); } if (newTestMerges) { description = String.Format(CultureInfo.InvariantCulture, "{0}est merge pull request(s) {1}{2}", description != null ? String.Format(CultureInfo.InvariantCulture, "{0} and t", description) : "T", String.Join(", ", model.NewTestMerges.Select(x => String.Format(CultureInfo.InvariantCulture, "#{0}{1}", x.Number, x.PullRequestRevision != null ? String.Format(CultureInfo.InvariantCulture, " at {0}", x.PullRequestRevision.Substring(0, 7)) : String.Empty))), description != null ? String.Empty : " in repository"); } if (description == null) { return(Json(api)); // no git changes } var job = new Models.Job { Description = description, StartedBy = AuthenticationContext.User, Instance = Instance, CancelRightsType = RightsType.Repository, CancelRight = (ulong)RepositoryRights.CancelPendingChanges, }; await jobManager.RegisterOperation(job, async (paramJob, databaseContext, progressReporter, ct) => { using (var repo = await repoManager.LoadRepository(ct).ConfigureAwait(false)) { if (repo == null) { throw new JobException("Repository could not be loaded!"); } var modelHasShaOrReference = model.CheckoutSha != null || model.Reference != null; var startReference = repo.Reference; var startSha = repo.Head; string postUpdateSha = null; if (newTestMerges && !repo.IsGitHubRepository) { throw new JobException("Cannot test merge on a non GitHub based repository!"); } var committerName = currentModel.ShowTestMergeCommitters.Value ? AuthenticationContext.User.Name : currentModel.CommitterName; var hardResettingToOriginReference = model.UpdateFromOrigin == true && model.Reference != null; var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1)); var doneSteps = 0; Action <int> NextProgressReporter() { var tmpDoneSteps = doneSteps; ++doneSteps; return(progress => progressReporter((progress + (100 * tmpDoneSteps)) / numSteps)); } progressReporter(0); // get a base line for where we are Models.RevisionInformation lastRevisionInfo = null; var attachedInstance = new Models.Instance { Id = Instance.Id }; databaseContext.Instances.Attach(attachedInstance); await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); // apply new rev info, tracking applied test merges async Task UpdateRevInfo() { var last = lastRevisionInfo; await LoadRevisionInformation(repo, databaseContext, attachedInstance, last.OriginCommitSha, x => lastRevisionInfo = x, ct).ConfigureAwait(false); lastRevisionInfo.ActiveTestMerges.AddRange(last.ActiveTestMerges); } try { // fetch/pull if (model.UpdateFromOrigin == true) { if (!repo.Tracking) { throw new JobException("Not on an updatable reference!"); } await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false); doneSteps = 1; if (!modelHasShaOrReference) { var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, NextProgressReporter(), ct).ConfigureAwait(false); if (!fastForward.HasValue) { throw new JobException("Merge conflict occurred during origin update!"); } await UpdateRevInfo().ConfigureAwait(false); if (fastForward.Value) { lastRevisionInfo.OriginCommitSha = repo.Head; await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false); postUpdateSha = repo.Head; } else { NextProgressReporter()(100); } } } // checkout/hard reset if (modelHasShaOrReference) { if ((model.CheckoutSha != null && repo.Head.ToUpperInvariant().StartsWith(model.CheckoutSha.ToUpperInvariant(), StringComparison.Ordinal)) || (model.Reference != null && repo.Reference.ToUpperInvariant() != model.Reference.ToUpperInvariant())) { var committish = model.CheckoutSha ?? model.Reference; var isSha = await repo.IsSha(committish, cancellationToken).ConfigureAwait(false); if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null)) { throw new JobException("Attempted to checkout a SHA or reference that was actually the opposite!"); } await repo.CheckoutObject(committish, NextProgressReporter(), ct).ConfigureAwait(false); await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); // we've either seen origin before or what we're checking out is on origin } else { NextProgressReporter()(100); } if (hardResettingToOriginReference) { if (!repo.Tracking) { throw new JobException("Checked out reference does not track a remote object!"); } await repo.ResetToOrigin(NextProgressReporter(), ct).ConfigureAwait(false); await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false); await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); // repo head is on origin so force this // will update the db if necessary lastRevisionInfo.OriginCommitSha = repo.Head; } } // test merging Dictionary <int, Octokit.PullRequest> prMap = null; if (newTestMerges) { // bit of sanitization foreach (var I in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.PullRequestRevision))) { I.PullRequestRevision = null; } var gitHubClient = currentModel.AccessToken != null ? gitHubClientFactory.CreateClient(currentModel.AccessToken) : (String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken) ? gitHubClientFactory.CreateClient() : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken)); var repoOwner = repo.GitHubOwner; var repoName = repo.GitHubRepoName; // optimization: if we've already merged these exact same commits in this fashion before, just find the rev info for it and check it out Models.RevisionInformation revInfoWereLookingFor = null; bool needToApplyRemainingPrs = true; if (lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha) { // In order for this to work though we need the shas of all the commits if (model.NewTestMerges.Any(x => x.PullRequestRevision == null)) { prMap = new Dictionary <int, Octokit.PullRequest>(); } bool cantSearch = false; foreach (var I in model.NewTestMerges) { if (I.PullRequestRevision != null) #pragma warning disable CA1308 // Normalize strings to uppercase { I.PullRequestRevision = I.PullRequestRevision?.ToLowerInvariant(); // ala libgit2 } #pragma warning restore CA1308 // Normalize strings to uppercase else { try { // retrieve the latest sha var pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number.Value).ConfigureAwait(false); prMap.Add(I.Number.Value, pr); I.PullRequestRevision = pr.Head.Sha; } catch { cantSearch = true; break; } } } if (!cantSearch) { var dbPull = await databaseContext.RevisionInformations .Where(x => x.Instance.Id == Instance.Id && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha && x.ActiveTestMerges.Count <= model.NewTestMerges.Count && x.ActiveTestMerges.Count > 0) .Include(x => x.ActiveTestMerges) .ThenInclude(x => x.TestMerge) .ToListAsync(cancellationToken).ConfigureAwait(false); // split here cause this bit has to be done locally revInfoWereLookingFor = dbPull .Where(x => x.ActiveTestMerges.Count == model.NewTestMerges.Count && x.ActiveTestMerges.Select(y => y.TestMerge) .All(y => model.NewTestMerges.Any(z => y.Number == z.Number && y.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal) && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null)))) .FirstOrDefault(); if (revInfoWereLookingFor == null && model.NewTestMerges.Count > 1) { // okay try to add at least SOME prs we've seen before var search = model.NewTestMerges.ToList(); var appliedTestMergeIds = new List <long>(); Models.RevisionInformation lastGoodRevInfo = null; do { foreach (var I in search) { revInfoWereLookingFor = dbPull .Where(x => model.NewTestMerges.Any(z => x.PrimaryTestMerge.Number == z.Number && x.PrimaryTestMerge.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal) && (x.PrimaryTestMerge.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null)) && x.ActiveTestMerges.Select(y => y.TestMerge).All(y => appliedTestMergeIds.Contains(y.Id))) .FirstOrDefault(); if (revInfoWereLookingFor != null) { lastGoodRevInfo = revInfoWereLookingFor; appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge.Id); search.Remove(I); break; } } }while (revInfoWereLookingFor != null && search.Count > 0); revInfoWereLookingFor = lastGoodRevInfo; needToApplyRemainingPrs = search.Count != 0; if (needToApplyRemainingPrs) { model.NewTestMerges = search; } } else if (revInfoWereLookingFor != null) { needToApplyRemainingPrs = false; } } } if (revInfoWereLookingFor != null) { // goteem await repo.ResetToSha(revInfoWereLookingFor.CommitSha, NextProgressReporter(), cancellationToken).ConfigureAwait(false); lastRevisionInfo = revInfoWereLookingFor; } if (needToApplyRemainingPrs) { // an invocation of LoadRevisionInformation could have already loaded this user var contextUser = databaseContext.Users.Local.Where(x => x.Id == AuthenticationContext.User.Id).FirstOrDefault(); if (contextUser == default) { contextUser = new Models.User { Id = AuthenticationContext.User.Id }; databaseContext.Users.Attach(contextUser); } else { Logger.LogTrace("Skipping attaching the user to the database context as it is already loaded!"); } foreach (var I in model.NewTestMerges) { Octokit.PullRequest pr = null; string errorMessage = null; if (lastRevisionInfo.ActiveTestMerges.Any(x => x.TestMerge.Number == I.Number.Value)) { throw new JobException("Cannot test merge the same PR twice in one HEAD!"); } try { // load from cache if possible if (prMap == null || !prMap.TryGetValue(I.Number.Value, out pr)) { pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number.Value).ConfigureAwait(false); } } catch (Octokit.RateLimitExceededException) { // you look at your anonymous access and sigh errorMessage = "P.R.E. RATE LIMITED"; } catch (Octokit.AuthorizationException) { errorMessage = "P.R.E. BAD CREDENTIALS"; } catch (Octokit.NotFoundException) { // you look at your shithub and sigh errorMessage = "P.R.E. NOT FOUND"; } // we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it if (I.PullRequestRevision == null && pr != null) { I.PullRequestRevision = pr.Head.Sha; } var mergeResult = await repo.AddTestMerge(I, committerName, currentModel.CommitterEmail, currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false); if (!mergeResult.HasValue) { throw new JobException(String.Format(CultureInfo.InvariantCulture, "Merge of PR #{0} at {1} conflicted!", I.Number, I.PullRequestRevision.Substring(0, 7))); } ++doneSteps; var revInfoUpdateTask = UpdateRevInfo(); var tm = new Models.TestMerge { Author = pr?.User.Login ?? errorMessage, BodyAtMerge = pr?.Body ?? errorMessage ?? String.Empty, MergedAt = DateTimeOffset.Now, TitleAtMerge = pr?.Title ?? errorMessage ?? String.Empty, Comment = I.Comment, Number = I.Number, MergedBy = contextUser, PullRequestRevision = I.PullRequestRevision, Url = pr?.HtmlUrl ?? errorMessage }; await revInfoUpdateTask.ConfigureAwait(false); lastRevisionInfo.PrimaryTestMerge = tm; lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge { TestMerge = tm }); } } } var currentHead = repo.Head; if (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead)) { await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), false, ct).ConfigureAwait(false); await UpdateRevInfo().ConfigureAwait(false); } await databaseContext.Save(ct).ConfigureAwait(false); } catch { doneSteps = 0; numSteps = 2; // the stuff didn't make it into the db, forget what we've done and abort await repo.CheckoutObject(startReference ?? startSha, NextProgressReporter(), default).ConfigureAwait(false); if (startReference != null && repo.Head != startSha) { await repo.ResetToSha(startSha, NextProgressReporter(), default).ConfigureAwait(false); } else { progressReporter(100); } throw; } } }, cancellationToken).ConfigureAwait(false); api.ActiveJob = job.ToApi(); return(Accepted(api)); }
public async Task <IActionResult> Create([FromBody] Repository model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (model.Origin == null) { return(BadRequest(new ErrorMessage { Message = "Missing repo origin!" })); } if (model.AccessUser == null ^ model.AccessToken == null) { return(BadRequest(new ErrorMessage { Message = "Either both accessToken and accessUser must be present or neither!" })); } var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (currentModel == default) { return(StatusCode((int)HttpStatusCode.Gone)); } // normalize github urls const string BadGitHubUrl = "://www.github.com/"; var uiOrigin = model.Origin.ToUpperInvariant(); var uiBad = BadGitHubUrl.ToUpperInvariant(); var uiGitHub = Components.Repository.Repository.GitHubUrl.ToUpperInvariant(); if (uiOrigin.Contains(uiBad, StringComparison.Ordinal)) { model.Origin = uiOrigin.Replace(uiBad, uiGitHub, StringComparison.Ordinal); } currentModel.AccessToken = model.AccessToken; currentModel.AccessUser = model.AccessUser; // intentionally only these fields, user not allowed to change anything else atm var cloneBranch = model.Reference; var origin = model.Origin; var repoManager = instanceManager.GetInstance(Instance).RepositoryManager; if (repoManager.CloneInProgress) { return(Conflict(new ErrorMessage { Message = "A clone operation is in progress!" })); } if (repoManager.InUse) { return(Conflict(new ErrorMessage { Message = "The repo is busy!" })); } using (var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false)) { // clone conflict if (repo != null) { return(Conflict(new ErrorMessage { Message = "The repository already exists!" })); } var job = new Models.Job { Description = String.Format(CultureInfo.InvariantCulture, "Clone branch {1} of repository {0}", origin, cloneBranch ?? "master"), StartedBy = AuthenticationContext.User, CancelRightsType = RightsType.Repository, CancelRight = (ulong)RepositoryRights.CancelClone, Instance = Instance }; var api = currentModel.ToApi(); await jobManager.RegisterOperation(job, async (paramJob, databaseContext, progressReporter, ct) => { using (var repos = await repoManager.CloneRepository(new Uri(origin), cloneBranch, currentModel.AccessUser, currentModel.AccessToken, progressReporter, ct).ConfigureAwait(false)) { if (repos == null) { throw new JobException("Filesystem conflict while cloning repository!"); } var instance = new Models.Instance { Id = Instance.Id }; databaseContext.Instances.Attach(instance); if (await PopulateApi(api, repos, databaseContext, instance, ct).ConfigureAwait(false)) { await databaseContext.Save(ct).ConfigureAwait(false); } } }, cancellationToken).ConfigureAwait(false); api.Origin = model.Origin; api.Reference = model.Reference; api.ActiveJob = job.ToApi(); return(StatusCode((int)HttpStatusCode.Created, api)); } }
#pragma warning disable CA1506 // TODO: Decomplexify public async Task <IActionResult> Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken) #pragma warning restore CA1506 { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (model.Version == null || model.Version.Revision != -1 || (model.Content != null && model.Version.Build > 0)) { return(BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure))); } var userByondRights = AuthenticationContext.InstanceUser.ByondRights.Value; if ((!userByondRights.HasFlag(ByondRights.InstallOfficialOrChangeActiveVersion) && model.Content == null) || (!userByondRights.HasFlag(ByondRights.InstallCustomVersion) && model.Content != null)) { return(Forbid()); } // remove cruff fields var result = new Api.Models.Byond(); return(await WithComponentInstance( async instance => { var byondManager = instance.ByondManager; if (model.Content == null && byondManager.InstalledVersions.Any(x => x == model.Version)) { Logger.LogInformation( "User ID {0} changing instance ID {1} BYOND version to {2}", AuthenticationContext.User.Id, Instance.Id, model.Version); await byondManager.ChangeVersion(model.Version, null, cancellationToken).ConfigureAwait(false); } else if (model.Version.Build > 0) { return BadRequest(new ErrorMessage(ErrorCode.ByondNonExistentCustomVersion)); } else { var installingVersion = model.Version.Build <= 0 ? new Version(model.Version.Major, model.Version.Minor) : model.Version; Logger.LogInformation( "User ID {0} installing BYOND version to {1} on instance ID {2}", AuthenticationContext.User.Id, installingVersion, Instance.Id); // run the install through the job manager var job = new Models.Job { Description = $"Install {(model.Content == null ? String.Empty : "custom ")}BYOND version {model.Version.Major}.{model.Version.Minor}", StartedBy = AuthenticationContext.User, CancelRightsType = RightsType.Byond, CancelRight = (ulong)ByondRights.CancelInstall, Instance = Instance }; await jobManager.RegisterOperation( job, (core, databaseContextFactory, paramJob, progressHandler, jobCancellationToken) => core.ByondManager.ChangeVersion( model.Version, model.Content, jobCancellationToken), cancellationToken) .ConfigureAwait(false); result.InstallJob = job.ToApi(); } if ((AuthenticationContext.GetRight(RightsType.Byond) & (ulong)ByondRights.ReadActive) != 0) { result.Version = byondManager.ActiveVersion; } return result.InstallJob != null ? (IActionResult)Accepted(result) : Json(result); }) .ConfigureAwait(false)); }
#pragma warning disable CA1502, CA1505 // TODO: Decomplexify public async Task <IActionResult> Update([FromBody] Repository model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (model.AccessUser == null ^ model.AccessToken == null) { return(BadRequest(new ErrorMessage(ErrorCode.RepoMismatchUserAndAccessToken))); } if (model.CheckoutSha != null && model.Reference != null) { return(BadRequest(new ErrorMessage(ErrorCode.RepoMismatchShaAndReference))); } if (model.CheckoutSha != null && model.UpdateFromOrigin == true) { return(BadRequest(new ErrorMessage(ErrorCode.RepoMismatchShaAndUpdate))); } if (model.NewTestMerges?.Any(x => model.NewTestMerges.Any(y => x != y && x.Number == y.Number)) == true) { return(BadRequest(new ErrorMessage(ErrorCode.RepoDuplicateTestMerge))); } if (model.CommitterName?.Length == 0) { return(BadRequest(new ErrorMessage(ErrorCode.RepoWhitespaceCommitterName))); } if (model.CommitterEmail?.Length == 0) { return(BadRequest(new ErrorMessage(ErrorCode.RepoWhitespaceCommitterEmail))); } var newTestMerges = model.NewTestMerges != null && model.NewTestMerges.Count > 0; var userRights = (RepositoryRights)AuthenticationContext.GetRight(RightsType.Repository); if (newTestMerges && !userRights.HasFlag(RepositoryRights.MergePullRequest)) { return(Forbid()); } var currentModel = await DatabaseContext .RepositorySettings .AsQueryable() .Where(x => x.InstanceId == Instance.Id) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (currentModel == default) { return(Gone()); } bool CheckModified <T>(Expression <Func <Api.Models.Internal.RepositorySettings, T> > expression, RepositoryRights requiredRight) { var memberSelectorExpression = (MemberExpression)expression.Body; var property = (PropertyInfo)memberSelectorExpression.Member; var newVal = property.GetValue(model); if (newVal == null) { return(false); } if (!userRights.HasFlag(requiredRight) && property.GetValue(currentModel) != newVal) { return(true); } property.SetValue(currentModel, newVal); return(false); } if (CheckModified(x => x.AccessToken, RepositoryRights.ChangeCredentials) || CheckModified(x => x.AccessUser, RepositoryRights.ChangeCredentials) || CheckModified(x => x.AutoUpdatesKeepTestMerges, RepositoryRights.ChangeAutoUpdateSettings) || CheckModified(x => x.AutoUpdatesSynchronize, RepositoryRights.ChangeAutoUpdateSettings) || CheckModified(x => x.CommitterEmail, RepositoryRights.ChangeCommitter) || CheckModified(x => x.CommitterName, RepositoryRights.ChangeCommitter) || CheckModified(x => x.PushTestMergeCommits, RepositoryRights.ChangeTestMergeCommits) || CheckModified(x => x.CreateGitHubDeployments, RepositoryRights.ChangeTestMergeCommits) || CheckModified(x => x.ShowTestMergeCommitters, RepositoryRights.ChangeTestMergeCommits) || CheckModified(x => x.PostTestMergeComment, RepositoryRights.ChangeTestMergeCommits) || (model.UpdateFromOrigin == true && !userRights.HasFlag(RepositoryRights.UpdateBranch))) { return(Forbid()); } if (model.AccessToken?.Length == 0 && model.AccessUser?.Length == 0) { // setting an empty string clears everything currentModel.AccessUser = null; currentModel.AccessToken = null; } var canRead = userRights.HasFlag(RepositoryRights.Read); var api = canRead ? currentModel.ToApi() : new Repository(); if (canRead) { var earlyOut = await WithComponentInstance( async instance => { var repoManager = instance.RepositoryManager; if (repoManager.CloneInProgress) { return(Conflict(new ErrorMessage(ErrorCode.RepoCloning))); } if (repoManager.InUse) { return(Conflict(new ErrorMessage(ErrorCode.RepoBusy))); } using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false); if (repo == null) { return(Conflict(new ErrorMessage(ErrorCode.RepoMissing))); } await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false); if (model.Origin != null && model.Origin != repo.Origin) { return(BadRequest(new ErrorMessage(ErrorCode.RepoCantChangeOrigin))); } return(null); }) .ConfigureAwait(false); if (earlyOut != null) { return(earlyOut); } } // this is just db stuf so stow it away await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); // format the job description string description = null; if (model.UpdateFromOrigin == true) { if (model.Reference != null) { description = String.Format(CultureInfo.InvariantCulture, "Fetch and hard reset repository to origin/{0}", model.Reference); } else if (model.CheckoutSha != null) { description = String.Format(CultureInfo.InvariantCulture, "Fetch and checkout {0} in repository", model.CheckoutSha); } else { description = "Pull current repository reference"; } } else if (model.Reference != null || model.CheckoutSha != null) { description = String.Format(CultureInfo.InvariantCulture, "Checkout repository {0} {1}", model.Reference != null ? "reference" : "SHA", model.Reference ?? model.CheckoutSha); } if (newTestMerges) { description = String.Format(CultureInfo.InvariantCulture, "{0}est merge(s) {1}{2}", description != null ? String.Format(CultureInfo.InvariantCulture, "{0} and t", description) : "T", String.Join(", ", model.NewTestMerges.Select(x => String.Format(CultureInfo.InvariantCulture, "#{0}{1}", x.Number, x.TargetCommitSha != null ? String.Format(CultureInfo.InvariantCulture, " at {0}", x.TargetCommitSha.Substring(0, 7)) : String.Empty))), description != null ? String.Empty : " in repository"); } if (description == null) { return(Json(api)); // no git changes } async Task <IActionResult> UpdateCallbackThatDesperatelyNeedsRefactoring( IInstanceCore instance, IDatabaseContextFactory databaseContextFactory, Action <int> progressReporter, CancellationToken ct) { var repoManager = instance.RepositoryManager; using var repo = await repoManager.LoadRepository(ct).ConfigureAwait(false); if (repo == null) { throw new JobException(ErrorCode.RepoMissing); } var modelHasShaOrReference = model.CheckoutSha != null || model.Reference != null; var startReference = repo.Reference; var startSha = repo.Head; string postUpdateSha = null; if (newTestMerges && repo.RemoteGitProvider == RemoteGitProvider.Unknown) { throw new JobException(ErrorCode.RepoUnsupportedTestMergeRemote); } var committerName = currentModel.ShowTestMergeCommitters.Value ? AuthenticationContext.User.Name : currentModel.CommitterName; var hardResettingToOriginReference = model.UpdateFromOrigin == true && model.Reference != null; var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1)); var doneSteps = 0; Action <int> NextProgressReporter() { var tmpDoneSteps = doneSteps; ++doneSteps; return(progress => progressReporter((progress + (100 * tmpDoneSteps)) / numSteps)); } progressReporter(0); // get a base line for where we are Models.RevisionInformation lastRevisionInfo = null; var attachedInstance = new Models.Instance { Id = Instance.Id }; Task CallLoadRevInfo(Models.TestMerge testMergeToAdd = null, string lastOriginCommitSha = null) => databaseContextFactory .UseContext( async databaseContext => { databaseContext.Instances.Attach(attachedInstance); var previousRevInfo = lastRevisionInfo; var needsUpdate = await LoadRevisionInformation( repo, databaseContext, attachedInstance, lastOriginCommitSha, x => lastRevisionInfo = x, ct) .ConfigureAwait(false); if (testMergeToAdd != null) { // rev info may have already loaded the user var mergedBy = databaseContext.Users.Local.FirstOrDefault(x => x.Id == AuthenticationContext.User.Id); if (mergedBy == default) { mergedBy = new Models.User { Id = AuthenticationContext.User.Id }; databaseContext.Users.Attach(mergedBy); } testMergeToAdd.MergedBy = mergedBy; foreach (var activeTestMerge in previousRevInfo.ActiveTestMerges) { lastRevisionInfo.ActiveTestMerges.Add(activeTestMerge); } lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge { TestMerge = testMergeToAdd }); lastRevisionInfo.PrimaryTestMerge = testMergeToAdd; needsUpdate = true; } if (needsUpdate) { await databaseContext.Save(cancellationToken).ConfigureAwait(false); } }); await CallLoadRevInfo().ConfigureAwait(false); // apply new rev info, tracking applied test merges Task UpdateRevInfo(Models.TestMerge testMergeToAdd = null) => CallLoadRevInfo(testMergeToAdd, lastRevisionInfo.OriginCommitSha); try { // fetch/pull if (model.UpdateFromOrigin == true) { if (!repo.Tracking) { throw new JobException(ErrorCode.RepoReferenceRequired); } await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false); doneSteps = 1; if (!modelHasShaOrReference) { var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, NextProgressReporter(), ct).ConfigureAwait(false); if (!fastForward.HasValue) { throw new JobException(ErrorCode.RepoMergeConflict); } lastRevisionInfo.OriginCommitSha = await repo.GetOriginSha(cancellationToken).ConfigureAwait(false); await UpdateRevInfo().ConfigureAwait(false); if (fastForward.Value) { await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false); postUpdateSha = repo.Head; } else { NextProgressReporter()(100); } } } // checkout/hard reset if (modelHasShaOrReference) { var validCheckoutSha = model.CheckoutSha != null && !repo.Head.StartsWith(model.CheckoutSha, StringComparison.OrdinalIgnoreCase); var validCheckoutReference = model.Reference != null && !repo.Reference.Equals(model.Reference, StringComparison.OrdinalIgnoreCase); if (validCheckoutSha || validCheckoutReference) { var committish = model.CheckoutSha ?? model.Reference; var isSha = await repo.IsSha(committish, cancellationToken).ConfigureAwait(false); if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null)) { throw new JobException(ErrorCode.RepoSwappedShaOrReference); } await repo.CheckoutObject(committish, NextProgressReporter(), ct).ConfigureAwait(false); await CallLoadRevInfo().ConfigureAwait(false); // we've either seen origin before or what we're checking out is on origin } else { NextProgressReporter()(100); } if (hardResettingToOriginReference) { if (!repo.Tracking) { throw new JobException(ErrorCode.RepoReferenceNotTracking); } await repo.ResetToOrigin(NextProgressReporter(), ct).ConfigureAwait(false); await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false); await CallLoadRevInfo().ConfigureAwait(false); // repo head is on origin so force this // will update the db if necessary lastRevisionInfo.OriginCommitSha = repo.Head; } } // test merging if (newTestMerges) { if (repo.RemoteGitProvider == RemoteGitProvider.Unknown) { throw new JobException(ErrorCode.RepoTestMergeInvalidRemote); } // bit of sanitization foreach (var I in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.TargetCommitSha))) { I.TargetCommitSha = null; } var gitHubClient = currentModel.AccessToken != null ? gitHubClientFactory.CreateClient(currentModel.AccessToken) : gitHubClientFactory.CreateClient(); var repoOwner = repo.RemoteRepositoryOwner; var repoName = repo.RemoteRepositoryName; // optimization: if we've already merged these exact same commits in this fashion before, just find the rev info for it and check it out Models.RevisionInformation revInfoWereLookingFor = null; bool needToApplyRemainingPrs = true; if (lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha) { bool cantSearch = false; foreach (var I in model.NewTestMerges) { if (I.TargetCommitSha != null) #pragma warning disable CA1308 // Normalize strings to uppercase { I.TargetCommitSha = I.TargetCommitSha?.ToLowerInvariant(); // ala libgit2 } #pragma warning restore CA1308 // Normalize strings to uppercase else { try { // retrieve the latest sha var pr = await repo.GetTestMerge(I, currentModel, ct).ConfigureAwait(false); // we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it I.TargetCommitSha = pr.TargetCommitSha; } catch { cantSearch = true; break; } } } if (!cantSearch) { List <Models.RevisionInformation> dbPull = null; await databaseContextFactory.UseContext( async databaseContext => dbPull = await databaseContext.RevisionInformations .AsQueryable() .Where(x => x.Instance.Id == Instance.Id && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha && x.ActiveTestMerges.Count <= model.NewTestMerges.Count && x.ActiveTestMerges.Count > 0) .Include(x => x.ActiveTestMerges) .ThenInclude(x => x.TestMerge) .ToListAsync(cancellationToken) .ConfigureAwait(false)) .ConfigureAwait(false); // split here cause this bit has to be done locally revInfoWereLookingFor = dbPull .Where(x => x.ActiveTestMerges.Count == model.NewTestMerges.Count && x.ActiveTestMerges.Select(y => y.TestMerge) .All(y => model.NewTestMerges.Any(z => y.Number == z.Number && y.TargetCommitSha.StartsWith(z.TargetCommitSha, StringComparison.Ordinal) && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null)))) .FirstOrDefault(); if (revInfoWereLookingFor == default && model.NewTestMerges.Count > 1) { // okay try to add at least SOME prs we've seen before var search = model.NewTestMerges.ToList(); var appliedTestMergeIds = new List <long>(); Models.RevisionInformation lastGoodRevInfo = null; do { foreach (var I in search) { revInfoWereLookingFor = dbPull .Where(testRevInfo => { if (testRevInfo.PrimaryTestMerge == null) { return(false); } var testMergeMatch = model.NewTestMerges.Any(testTestMerge => { var numberMatch = testRevInfo.PrimaryTestMerge.Number == testTestMerge.Number; if (!numberMatch) { return(false); } var shaMatch = testRevInfo.PrimaryTestMerge.TargetCommitSha.StartsWith( testTestMerge.TargetCommitSha, StringComparison.Ordinal); if (!shaMatch) { return(false); } var commentMatch = testRevInfo.PrimaryTestMerge.Comment == testTestMerge.Comment; return(commentMatch); }); if (!testMergeMatch) { return(false); } var previousTestMergesMatch = testRevInfo .ActiveTestMerges .Select(previousRevInfoTestMerge => previousRevInfoTestMerge.TestMerge) .All(previousTestMerge => appliedTestMergeIds.Contains(previousTestMerge.Id)); return(previousTestMergesMatch); }) .FirstOrDefault(); if (revInfoWereLookingFor != null) { lastGoodRevInfo = revInfoWereLookingFor; appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge.Id); search.Remove(I); break; } } }while (revInfoWereLookingFor != null && search.Count > 0); revInfoWereLookingFor = lastGoodRevInfo; needToApplyRemainingPrs = search.Count != 0; if (needToApplyRemainingPrs) { model.NewTestMerges = search; } } else if (revInfoWereLookingFor != null) { needToApplyRemainingPrs = false; } } } if (revInfoWereLookingFor != null) { // goteem Logger.LogDebug("Reusing existing SHA {0}...", revInfoWereLookingFor.CommitSha); await repo.ResetToSha(revInfoWereLookingFor.CommitSha, NextProgressReporter(), cancellationToken).ConfigureAwait(false); lastRevisionInfo = revInfoWereLookingFor; } if (needToApplyRemainingPrs) { foreach (var I in model.NewTestMerges) { if (lastRevisionInfo.ActiveTestMerges.Any(x => x.TestMerge.Number == I.Number)) { throw new JobException(ErrorCode.RepoDuplicateTestMerge); } var fullTestMergeTask = repo.GetTestMerge(I, currentModel, ct); var mergeResult = await repo.AddTestMerge( I, committerName, currentModel.CommitterEmail, currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false); if (mergeResult == null) { throw new JobException( ErrorCode.RepoTestMergeConflict, new JobException( $"Test Merge #{I.Number} at {I.TargetCommitSha.Substring(0, 7)} conflicted!")); } Models.TestMerge fullTestMerge; try { fullTestMerge = await fullTestMergeTask.ConfigureAwait(false); } catch (Exception ex) { Logger.LogWarning("Error retrieving metadata for test merge #{0}!", I.Number); fullTestMerge = new Models.TestMerge { Author = ex.Message, BodyAtMerge = ex.Message, MergedAt = DateTimeOffset.UtcNow, TitleAtMerge = ex.Message, Comment = I.Comment, Number = I.Number, Url = ex.Message }; } // Ensure we're getting the full sha from git itself fullTestMerge.TargetCommitSha = I.TargetCommitSha; // MergedBy will be set later ++doneSteps; await UpdateRevInfo(fullTestMerge).ConfigureAwait(false); } } } var currentHead = repo.Head; if (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead)) { await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), false, ct).ConfigureAwait(false); await UpdateRevInfo().ConfigureAwait(false); } return(null); } catch { doneSteps = 0; numSteps = 2; // Forget what we've done and abort // DCTx2: Cancellation token is for job, operations should always run await repo.CheckoutObject(startReference ?? startSha, NextProgressReporter(), default).ConfigureAwait(false); if (startReference != null && repo.Head != startSha) { await repo.ResetToSha(startSha, NextProgressReporter(), default).ConfigureAwait(false); } else { progressReporter(100); } throw; } } var job = new Models.Job { Description = description, StartedBy = AuthenticationContext.User, Instance = Instance, CancelRightsType = RightsType.Repository, CancelRight = (ulong)RepositoryRights.CancelPendingChanges, }; // Time to access git, do it in a job await jobManager.RegisterOperation( job, (core, databaseContextFactory, paramJob, progressReporter, ct) => UpdateCallbackThatDesperatelyNeedsRefactoring( core, databaseContextFactory, progressReporter, ct), cancellationToken) .ConfigureAwait(false); api.ActiveJob = job.ToApi(); return(Accepted(api)); }
public async Task <IActionResult> Create([FromBody] Repository model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (model.Origin == null) { return(BadRequest(ErrorCode.RepoMissingOrigin)); } if (model.AccessUser == null ^ model.AccessToken == null) { return(BadRequest(ErrorCode.RepoMismatchUserAndAccessToken)); } var currentModel = await DatabaseContext .RepositorySettings .AsQueryable() .Where(x => x.InstanceId == Instance.Id) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (currentModel == default) { return(Gone()); } currentModel.AccessToken = model.AccessToken; currentModel.AccessUser = model.AccessUser; // intentionally only these fields, user not allowed to change anything else atm var cloneBranch = model.Reference; var origin = model.Origin; return(await WithComponentInstance( async instance => { var repoManager = instance.RepositoryManager; if (repoManager.CloneInProgress) { return Conflict(new ErrorMessage(ErrorCode.RepoCloning)); } if (repoManager.InUse) { return Conflict(new ErrorMessage(ErrorCode.RepoBusy)); } using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false); // clone conflict if (repo != null) { return Conflict(new ErrorMessage(ErrorCode.RepoExists)); } var job = new Models.Job { Description = String.Format(CultureInfo.InvariantCulture, "Clone branch {1} of repository {0}", origin, cloneBranch ?? "master"), StartedBy = AuthenticationContext.User, CancelRightsType = RightsType.Repository, CancelRight = (ulong)RepositoryRights.CancelClone, Instance = Instance }; var api = currentModel.ToApi(); await jobManager.RegisterOperation(job, async(core, databaseContextFactory, paramJob, progressReporter, ct) => { var repoManager = core.RepositoryManager; using var repos = await repoManager.CloneRepository( origin, cloneBranch, currentModel.AccessUser, currentModel.AccessToken, progressReporter, model.RecurseSubmodules ?? true, ct) .ConfigureAwait(false); if (repos == null) { throw new JobException(ErrorCode.RepoExists); } var instance = new Models.Instance { Id = Instance.Id }; await databaseContextFactory.UseContext( async databaseContext => { databaseContext.Instances.Attach(instance); if (await PopulateApi(api, repos, databaseContext, instance, ct).ConfigureAwait(false)) { await databaseContext.Save(ct).ConfigureAwait(false); } }) .ConfigureAwait(false); }, cancellationToken).ConfigureAwait(false); api.Origin = model.Origin; api.Reference = model.Reference; api.ActiveJob = job.ToApi(); return Created(api); }) .ConfigureAwait(false)); }