// Copyright Epic Games, Inc. All Rights Reserved. #include "ModelInterface.h" #include "SubmitToolUserPrefs.h" #include "Logic/JiraService.h" #include "Logic/PreflightService.h" #include "Framework/Application/SlateApplication.h" #include "Version/AppVersion.h" #include "Widgets/Docking/SDockTab.h" #include "Containers/Ticker.h" #include "Configuration/Configuration.h" #include "SourceControlOperations.h" #include "HAL/PlatformApplicationMisc.h" #include "Async/Async.h" #include "Logic/Validators/PreflightValidator.h" #include "Logic/Validators/TagValidator.h" #include "Logic/Validators/UBTValidator.h" #include "Logic/Validators/EditorCommandletValidator.h" #include "Logic/Validators/ValidatorBase.h" #include "Logic/Validators/ValidatorRunExecutable.h" #include "Logic/Validators/CrossChangelistValidator.h" #include "Logic/Validators/PackageDataValidator.h" #include "Logic/Validators/ShaderValidator.h" #include "Logic/Validators/JsonValidator.h" #include "Logic/PreSubmitOperations/VirtualizationOperation.h" #include "Logic/Services/CacheDataService.h" #include "Logic/Services/SourceControl/SubmitToolPerforce.h" ESubmitToolAppState FModelInterface::SubmitToolState = ESubmitToolAppState::Initializing; FOnStateChanged FModelInterface::OnStateChanged; FModelInterface::FModelInterface(const FSubmitToolParameters& InParameters) : Parameters(InParameters) { // initialize call backs CLReadyCallback = FOnChangeListReadyDelegate::CreateRaw(this, &FModelInterface::OnChangelistReady); CLRefreshCallback = FOnChangelistRefreshDelegate::CreateRaw(this, &FModelInterface::OnChangelistRefresh); SubmitFinishedCallback = FSourceControlOperationComplete::CreateRaw(this, &FModelInterface::OnSubmitOperationComplete); DeleteShelveCallback = FSourceControlOperationComplete::CreateRaw(this, &FModelInterface::OnDeleteShelveOperationComplete); RevertUnchangedCallback = FSourceControlOperationComplete::CreateRaw(this, &FModelInterface::OnRevertUnchangedOperationComplete); ServiceProvider = MakeShared(); // Initialize services if (Parameters.GeneralParameters.CacheFile.IsEmpty()) { ServiceProvider->RegisterService(MakeShared()); } else { ServiceProvider->RegisterService(MakeShared(Parameters.GeneralParameters)); } SourceControlService = MakeShared(InParameters); ServiceProvider->RegisterService(SourceControlService.ToSharedRef()); ValidationService = MakeShared(InParameters.Validators, TEXT("SubmitTool.StandAlone.Validator")); ServiceProvider->RegisterService(ValidationService.ToSharedRef(), TEXT("ValidationService")); PresubmitOperationsService = MakeShared(InParameters.PresubmitOperations, TEXT("SubmitTool.StandAlone.PresubmitOperation")); ServiceProvider->RegisterService(PresubmitOperationsService.ToSharedRef(), TEXT("PresubmitOperationsService")); CredentialsService = MakeShared(InParameters.OAuthParameters); ServiceProvider->RegisterService(CredentialsService.ToSharedRef()); ChangelistService = MakeShared(InParameters.GeneralParameters, SourceControlService, CLReadyCallback, CLRefreshCallback); ServiceProvider->RegisterService(ChangelistService.ToSharedRef()); P4LockdownService = MakeShared(InParameters.P4LockdownParameters, ServiceProvider); ServiceProvider->RegisterService(P4LockdownService.ToSharedRef()); TagService = MakeShared(InParameters, ChangelistService); ServiceProvider->RegisterService(TagService.ToSharedRef()); SwarmService = MakeShared(ServiceProvider); ServiceProvider->RegisterService(SwarmService.ToSharedRef()); PreflightService = MakeShared(InParameters.HordeParameters, this, ServiceProvider); ServiceProvider->RegisterService(PreflightService.ToSharedRef()); JiraService = MakeShared(InParameters.JiraParameters, 256, ServiceProvider); ServiceProvider->RegisterService(JiraService.ToSharedRef()); FNIntegrationService = MakeShared(InParameters.IntegrationParameters, ServiceProvider); ServiceProvider->RegisterService(FNIntegrationService.ToSharedRef()); UpdateService = MakeShared(InParameters.HordeParameters, InParameters.AutoUpdateParameters, ServiceProvider); ServiceProvider->RegisterService(UpdateService.ToSharedRef()); ParseValidators(); ParsePreSubmitOperations(); OnValidationStateUpdatedHandle = ValidationService->OnTasksRunResultUpdated.Add(FOnTaskRunStateChanged::FDelegate::CreateLambda([this](bool bIsValid) { if (bIsValid) { bool bOptionalFailures = false; for (const TWeakPtr& Validator : ValidationService->GetTasks()) { if (!Validator.Pin()->GetIsRunningOrQueued() && !Validator.Pin()->GetHasPassed()) { bOptionalFailures = true; } } UE_LOG(LogSubmitTool, Log, TEXT("The required local validation has succeeded, you're ALLOWED TO SUBMIT.")) if (ValidationService->GetIsAnyTaskRunning()) { UE_LOG(LogSubmitTool, Warning, TEXT("You still have optional validations running you might want to consider waiting for them to finish.")) } if (bOptionalFailures) { UE_LOG(LogSubmitTool, Warning, TEXT("You have optional validations that have failed, you can still proceed with the submission if you consider that these failures are not relevant. Please make sure this is the case.")) } } })); OnSingleValidationFinishedHandle = ValidationService->OnSingleTaskFinished.AddLambda([this](const FValidatorBase& InTask) { ReevaluateSubmitToolTag(); if (bPreflightQueued && CanLaunchPreflight()) { bPreflightQueued = false; PreflightService->RequestPreflight(); } }); OnValidationFinishedHandle = ValidationService->OnTasksQueueFinished.Add(FOnTaskFinished::FDelegate::CreateLambda([this](bool bIsValid) { MainTab.Pin()->GetParentWindow()->DrawAttention(FWindowDrawAttentionParameters()); if (bIsValid) { if (!bPreflightQueued && bSubmitOnSuccessfulValidation && !IsIntegrationRequired()) { bool bAllSucceedIncludingOptional = true; for (const TWeakPtr& Validator : ValidationService->GetTasks()) { if (Validator.IsValid() && !Validator.Pin()->GetHasPassed()) { bAllSucceedIncludingOptional = false; } } if (bAllSucceedIncludingOptional) { UE_LOG(LogSubmitTool, Log, TEXT("User has opted in to automatically submit on validation successful. Proceeding with submission...")); StartSubmitProcess(true); } else { UE_LOG(LogSubmitTool, Warning, TEXT("User has opted in to automatically submit on validation successful but not all validations succeeded. Fix optional validation errors or submit manually if you want to bypass them.")); FDialogFactory::ShowInformationDialog(FText::FromString(TEXT("Auto-Submit Cancelled")), FText::FromString(TEXT("Submit tool couldn't auto submit because there were optional validations that failed.\n\nFix these errors or manually submit if you are certain that you should ignore them."))); bSubmitOnSuccessfulValidation = false; } } } })); OnPresubmitFinishedHandle = PresubmitOperationsService->OnTasksQueueFinished.Add(FOnTaskFinished::FDelegate::CreateRaw(this, &FModelInterface::OnPresubmitOperationsComplete)); FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateRaw(this, &FModelInterface::Tick)); } FModelInterface::~FModelInterface() { PrepareSubmitCallBack.Clear(); FileRefreshedCallback.Clear(); ValidationService->OnTasksRunResultUpdated.Remove(OnValidationStateUpdatedHandle); ValidationService->OnTasksQueueFinished.Remove(OnValidationFinishedHandle); ValidationService->OnSingleTaskFinished.Remove(OnSingleValidationFinishedHandle); PresubmitOperationsService->OnTasksQueueFinished.Remove(OnPresubmitFinishedHandle); ServiceProvider.Reset(); } void FModelInterface::Dispose() const { ChangelistService->CancelP4Operations(); if (GetInputEnabled()) { ChangelistService->SendCLDescriptionToP4(EConcurrency::Synchronous); } for (const TPair>& IntegrationOption : ServiceProvider->GetService()->GetIntegrationOptions()) { FString Value; if (IntegrationOption.Value->GetJiraValue(Value) && !Value.IsEmpty()) { ServiceProvider->GetService()->SetIntegrationFieldValue(GetCLID(), IntegrationOption.Key, Value); } } ServiceProvider->GetService()->SaveCacheToDisk(); ValidationService->StopTasks(); } void FModelInterface::ParseValidators() const { TArray> Tasks; for (const TPair DefinitionPair : Parameters.Validators) { FValidatorDefinition TaskDefinition; FStringOutputDevice Errors; FValidatorDefinition::StaticStruct()->ImportText(*DefinitionPair.Value, &TaskDefinition, nullptr, 0, &Errors, FValidatorDefinition::StaticStruct()->GetName()); if (!Errors.IsEmpty()) { UE_LOG(LogSubmitTool, Error, TEXT("Error loading parameter file %s"), *Errors); FModelInterface::SetErrorState(); continue; } if (TaskDefinition.Type.TrimStartAndEnd().IsEmpty()) { UE_LOG(LogSubmitTool, Error, TEXT("Task %s didn't have a Type."), *DefinitionPair.Key.ToString()); continue; } if (TaskDefinition.bIsDisabled) { UE_LOG(LogSubmitToolDebug, Log, TEXT("Task %s was disabled by configuration"), *DefinitionPair.Key.ToString()); continue; } if (TaskDefinition.Type.Equals(SubmitToolParseConstants::TagValidator, ESearchCase::IgnoreCase)) { Tasks.Add(MakeShared(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value)); } else if (TaskDefinition.Type.Equals(SubmitToolParseConstants::UBTValidator, ESearchCase::IgnoreCase)) { Tasks.Add(MakeShared(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value)); } else if (TaskDefinition.Type.Equals(SubmitToolParseConstants::EditorValidator, ESearchCase::IgnoreCase)) { Tasks.Add(MakeShared(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value)); } else if (TaskDefinition.Type.Equals(SubmitToolParseConstants::CustomValidator, ESearchCase::IgnoreCase)) { Tasks.Add(MakeShared(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value)); } else if (TaskDefinition.Type.Equals(SubmitToolParseConstants::CrossChangelistValidator, ESearchCase::IgnoreCase)) { Tasks.Add(MakeShared(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value)); } else if (TaskDefinition.Type.Equals(SubmitToolParseConstants::PreflightValidator, ESearchCase::IgnoreCase)) { Tasks.Add(MakeShared(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value)); } else if (TaskDefinition.Type.Equals(SubmitToolParseConstants::PackageDataValidator, ESearchCase::IgnoreCase)) { Tasks.Add(MakeShared(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value)); } else if (TaskDefinition.Type.Equals(SubmitToolParseConstants::ShaderValidator, ESearchCase::IgnoreCase)) { Tasks.Add(MakeShared(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value)); } else if (TaskDefinition.Type.Equals(SubmitToolParseConstants::JsonValidator, ESearchCase::IgnoreCase)) { Tasks.Add(MakeShared(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value)); } else { UE_LOG(LogSubmitTool, Error, TEXT("[%s] is not a recognized validator type and has not been activated."), *DefinitionPair.Key.ToString()); } } ValidationService->InitializeTasks(Tasks); } void FModelInterface::ParsePreSubmitOperations() const { TArray> Tasks; for (const TPair DefinitionPair : Parameters.PresubmitOperations) { FValidatorDefinition TaskDefinition; FStringOutputDevice Errors; FValidatorDefinition::StaticStruct()->ImportText(*DefinitionPair.Value, &TaskDefinition, nullptr, 0, &Errors, FValidatorDefinition::StaticStruct()->GetName()); if (!Errors.IsEmpty()) { UE_LOG(LogSubmitTool, Error, TEXT("Error loading parameter file %s"), *Errors); FModelInterface::SetErrorState(); continue; } if (TaskDefinition.Type.TrimStartAndEnd().IsEmpty()) { UE_LOG(LogSubmitTool, Error, TEXT("Task %s didn't have a Type."), *DefinitionPair.Key.ToString()); continue; } if (TaskDefinition.Type.Equals(SubmitToolParseConstants::CustomValidator, ESearchCase::IgnoreCase)) { Tasks.Add(MakeShared(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value)); } else if (TaskDefinition.Type.Equals(SubmitToolParseConstants::VirtualizationToolOp, ESearchCase::IgnoreCase)) { Tasks.Add(MakeShared(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value)); } else { UE_LOG(LogSubmitTool, Error, TEXT("[%s] is not a recognized pre submit operation type and has not been activated."), *DefinitionPair.Key.ToString()); } } PresubmitOperationsService->InitializeTasks(Tasks); } void FModelInterface::SetCLDescription(const FText& newDescription, bool DoNotInvalidate) const { if (ChangelistService->SetCLDescription(newDescription.ToString())) { TagService->ParseCLDescription(); if (!DoNotInvalidate) { ValidationService->InvalidateForChanges(ETaskArea::Changelist); } } } void FModelInterface::SendDescriptionToP4() const { if (GetInputEnabled()) { if (IsP4OperationRunning()) { UE_LOG(LogSubmitToolP4, Log, TEXT("Attempted to send description to P4, but another operation is already running")); return; } ChangelistService->SendCLDescriptionToP4(); } } bool FModelInterface::CanLaunchPreflight() const { // Check Validators which are validating files, ignore changelist (description, valid tags) validators when we evaluate if we // allow the user to trigger a preflight for (const TWeakPtr& Validator : ValidationService->GetTasks()) { if (Validator.IsValid()) { const TSharedPtr Pinned = Validator.Pin(); if ((Pinned->Definition->TaskArea & ETaskArea::ShelveAndLocalFiles) != ETaskArea::None) { if ((Pinned->Definition->IsRequired && !Pinned->GetHasPassed()) || (!Pinned->Definition->IsRequired && Pinned->GetIsRunningOrQueued())) { return false; } } } } return true; } void FModelInterface::ReevaluateSubmitToolTag() { UpdateSubmitToolTag(ValidationService->GetIsRunSuccessful(!IsIntegrationRequired())); } void FModelInterface::UpdateSubmitToolTag(bool InbAdd) { // add a special tag to the CL description FString SubmitToolTag = FString::Format(TEXT("#submittool {0}\n"), { FAppVersion::GetVersion() }); FString DescriptionCopy = ChangelistService->GetCLDescription(); if (InbAdd) { if (!HasSubmitToolTag()) { UE_LOG(LogSubmitToolDebug, Log, TEXT("Added Submit Tool tag")); DescriptionCopy.Append(TEXT("\n") + SubmitToolTag); ChangelistService->SetCLDescription(DescriptionCopy, true); } } else if (HasSubmitToolTag()) { FString VersionlessTag = TEXT("#submittool "); int32 Loc = DescriptionCopy.Find(VersionlessTag); if (Loc >= 0) { size_t EndPos = Loc + VersionlessTag.Len(); while (EndPos < DescriptionCopy.Len()) { if (DescriptionCopy[EndPos] == TCHAR('\n')) { ++EndPos; break; } ++EndPos; } if (Loc != 0 && DescriptionCopy[Loc - 1] == '\n' && (EndPos - DescriptionCopy.Len()) < 2) { Loc--; } UE_LOG(LogSubmitToolDebug, Log, TEXT("Removed Submit Tool tag")); DescriptionCopy.RemoveAt(Loc, EndPos - Loc); ChangelistService->SetCLDescription(DescriptionCopy, true); TagService->ParseCLDescription(); } } } bool FModelInterface::HasSubmitToolTag() const { // Only Checking that it has a submit tool tag, regardless of version. return ChangelistService->GetCLDescription().Find(TEXT("#submittool ")) != INDEX_NONE; } void FModelInterface::UpdateCLFromP4Async() const { if (GetInputEnabled() || SubmitToolState == ESubmitToolAppState::Errored || SubmitToolState == ESubmitToolAppState::SubmitLocked) { ChangelistService->FetchChangelistDataAsync(); } } bool FModelInterface::GetInputEnabled() { return SubmitToolState == ESubmitToolAppState::WaitingUserInput || SubmitToolState == ESubmitToolAppState::SubmitLocked; } void FModelInterface::RequestPreflight() { if (CanLaunchPreflight()) { PreflightService->RequestPreflight(); } else { bPreflightQueued = true; } } void FModelInterface::ShowSwarmReview() { if (HasSwarmReview() && SwarmService.IsValid()) { FString Url; if (SwarmService->GetCurrentReviewUrl(Url)) { UE_LOG(LogSubmitTool, Log, TEXT("Swarm: Opening Swarm Review with URL: \"%s\""), *Url); FPlatformProcess::LaunchURL(*Url, nullptr, nullptr); } } } void FModelInterface::RequestSwarmReview(const TArray& InReviewers) { if (!HasSwarmReview() && SwarmService.IsValid()) { if (!HasShelvedFiles()) { ChangelistService->CreateShelvedFiles(FSourceControlOperationComplete::CreateLambda([this, InReviewers](const FSourceControlOperationRef& DeleteShelvedOp, ECommandResult::Type Result) { if (Result == ECommandResult::Succeeded && HasShelvedFiles()) { RequestSwarmReview(InReviewers); } else { UE_LOG(LogSubmitTool, Error, TEXT("Failed to shelve files, Swarm Review request is cancelled")); } })); return; } SwarmService->CreateReview(InReviewers, OnCreateReviewComplete::CreateRaw(this, &FModelInterface::OnSwarmCreateCompleted)); } } void FModelInterface::StartSubmitProcess(bool bSkipShelfDialog) { PresubmitOperationsService->ResetStates(); // Check if any last minute file changes have come in that invalidated any validators. CheckForFileEdits(); if (IsCLValid()) { if (PrepareSubmitCallBack.IsBound()) { PrepareSubmitCallBack.Broadcast(); } UpdateSubmitToolTag(true); if (Parameters.IncompatibleFilesParams.IncompatibleFileGroups.Num() > 0) { const TArray& FilesInCL = ChangelistService->GetFilesDepotPaths(); FConfiguration::AddOrUpdateEntry(TEXT("$(StreamRoot)"), *GetRootStreamName()); for (const FIncompatibleFilesGroup& FileGroup : Parameters.IncompatibleFilesParams.IncompatibleFileGroups) { TArray> Indexes; for (const FString& File : FilesInCL) { for (size_t i = 0; i < FileGroup.FileGroups.Num(); ++i) { if (File.Contains(FConfiguration::Substitute(FileGroup.FileGroups[i]), ESearchCase::IgnoreCase)) { if (!Indexes.Contains(i)) { Indexes.Add(i); } break; } } } if (Indexes.Num() > 1) { const FText TextTitle = FText::FromString(FileGroup.Title); const FText TextDescription = FText::FromString(FileGroup.GetMessage()); if (FileGroup.bIsError) { FDialogFactory::ShowInformationDialog(TextTitle, TextDescription); UE_LOG(LogSubmitTool, Log, TEXT("Submission canceled due to incompatible files")); return; } else { if (FDialogFactory::ShowConfirmDialog(TextTitle, TextDescription) != EDialogFactoryResult::Confirm) { UE_LOG(LogSubmitTool, Log, TEXT("Submission canceled by user")); return; } } } } } if (HasShelvedFiles()) { const TArray& ShelvedFiles = ChangelistService->GetShelvedFilesDepotPaths(); const TArray& LocalFiles = ChangelistService->GetFilesDepotPaths(); EDialogFactoryResult DialogResult; if (ShelvedFiles != LocalFiles) { const FText TextTitle = NSLOCTEXT("SourceControl.SubmitWindow", "ShelveConflictTitle", "Shelve - Local conflict"); const size_t MaxFilesToList = 5; TArray ShelvedList; TArray LocalList; for (size_t i = 0; i < MaxFilesToList; ++i) { if (i < ShelvedFiles.Num()) { ShelvedList.Add(ShelvedFiles[i]); } if (i < LocalFiles.Num()) { LocalList.Add(LocalFiles[i]); } } FString LocalListString = FString::Join(LocalList, TEXT("\n - ")); if (LocalFiles.Num() > MaxFilesToList) { LocalListString = FString::Printf(TEXT("%s\n - And %d other files"), *LocalListString, LocalFiles.Num() - MaxFilesToList); } FString ShelveListString = FString::Join(ShelvedList, TEXT("\n - ")); if (ShelvedFiles.Num() > MaxFilesToList) { ShelveListString = FString::Printf(TEXT("%s\n - And %d other files"), *ShelveListString, ShelvedFiles.Num() - MaxFilesToList); } if (bSkipShelfDialog) { DialogResult = EDialogFactoryResult::Confirm; } else { const FString Description = FString::Printf(TEXT("The shelve filelist does not match the local filelist, due to p4 restrictions submit tool can only submit local content do you want to continue with the submit?\nLocal Files:\n - %s\n\nShelved Files:\n - %s"), *LocalListString, *ShelveListString); DialogResult = FDialogFactory::ShowDialog(TextTitle, FText::FromString(Description), TArray{TEXT("Delete Shelve and Submit"), TEXT("Cancel")}); } } else { if (bSkipShelfDialog) { DialogResult = EDialogFactoryResult::Confirm; } else { const FText TextTitle = NSLOCTEXT("SourceControl.SubmitWindow", "DeleteShelvedFilesDialogTitle", "Delete shelved files?"); const FText TextDescription = NSLOCTEXT("SourceControl.SubmitWindow", "DeleteShelvedFilesDialogDescription", "There are shelved files in this changelist. Do you want to delete your shelf?\nIf you do not, the submit will be cancelled."); DialogResult = FDialogFactory::ShowConfirmDialog(TextTitle, TextDescription); } } if (DialogResult == EDialogFactoryResult::Confirm) { ChangeState(ESubmitToolAppState::Submitting, SubmitToolState == ESubmitToolAppState::SubmitLocked && bIsUserInAllowlist); DeleteShelvedFiles(); } } else { ChangeState(ESubmitToolAppState::Submitting, SubmitToolState == ESubmitToolAppState::SubmitLocked && bIsUserInAllowlist); RevertUnchangedAndSubmit(); } } else { UE_LOG(LogSubmitTool, Warning, TEXT("Attempted to submit, but all validators have not passed. Aborting submit.")); } } void FModelInterface::RequestIntegration() const { FNIntegrationService->RequestIntegration(FOnBooleanValueChanged::CreateLambda([this](bool bSuccess) { if (bSuccess) { ChangeState(ESubmitToolAppState::Finished); } })); } void FModelInterface::RefreshStateBasedOnFiles() { TArray LocalFiles = ChangelistService->GetFilesInCL(); if (LocalFiles.IsEmpty()) { const TArray& ShelvedFiles = ChangelistService->GetShelvedFilesInCL(); if (ShelvedFiles.IsEmpty()) { UE_LOG(LogSubmitTool, Error, TEXT("There are no files in CL %s, SUBMIT IS DISABLED"), *ChangelistService->GetCLID()); ChangeState(ESubmitToolAppState::Errored); } else { if (!HasSubmitToolTag()) { UE_LOG(LogSubmitTool, Warning, TEXT("This CL hasn't been validated and there are no local files. You need to unshelve and run validations."), *ChangelistService->GetCLID()); ChangeState(ESubmitToolAppState::Errored); } else { UE::Tasks::Launch(UE_SOURCE_LOCATION, [this] { if (P4LockdownService->ArePathsInLockdown(ChangelistService->GetShelvedFilesDepotPaths(), bIsUserInAllowlist)) { UE_LOG(LogSubmitTool, Log, TEXT("There are no local files in CL %s, Submit is disabled but you can still request an Integration with your shelved files"), *ChangelistService->GetCLID()); AsyncTask(ENamedThreads::GameThread, [] { ChangeState(ESubmitToolAppState::SubmitLocked); }); } else { UE_LOG(LogSubmitTool, Error, TEXT("There are no files in CL %s, SUBMIT IS DISABLED"), *ChangelistService->GetCLID()); AsyncTask(ENamedThreads::GameThread, [] { ChangeState(ESubmitToolAppState::Errored); }); } }); } } } else { const TArray& Streams = SourceControlService->GetClientStreams(); if (!Streams.IsEmpty()) { FString StreamsMsg = FString::JoinBy(Streams, TEXT(" -> "), [](const FSCCStream* InStr) { return InStr->Name; }); for (const FString& File : ChangelistService->GetFilesDepotPaths()) { bool bMappedToView = false; for (const FSCCStream* Str : Streams) { if (File.StartsWith(Str->Name)) { bMappedToView = true; break; } for (const FString& ImportStream : Str->AdditionalImportPaths) { if (File.StartsWith(ImportStream)) { bMappedToView = true; break; } } } if (!bMappedToView) { UE_LOG(LogSubmitTool, Warning, TEXT("File %s is not in the stream that the workspace is set to: %s"), *File, *StreamsMsg); } } } UE::Tasks::Launch(UE_SOURCE_LOCATION, [this] { if (P4LockdownService->ArePathsInLockdown(ChangelistService->GetFilesDepotPaths(), bIsUserInAllowlist)) { AsyncTask(ENamedThreads::GameThread, [] { ChangeState(ESubmitToolAppState::SubmitLocked); }); } else { AsyncTask(ENamedThreads::GameThread, [] { ChangeState(ESubmitToolAppState::WaitingUserInput); }); } }); } } void FModelInterface::OnChangelistReady(bool bIsValid) { if (SubmitToolState == ESubmitToolAppState::Initializing) { if (bIsValid) { UE_LOG(LogSubmitTool, Log, TEXT("Retrieved information for CL %s"), *ChangelistService->GetCLID()); PreflightService->FetchPreflightInfo(true); TagService->ParseCLDescription(); SwarmService->FetchReview(OnGetReviewComplete::CreateRaw(this, &FModelInterface::OnGetUsersFromSwarmCompleted)); RefreshStateBasedOnFiles(); if (!ChangelistService->GetFilesInCL().IsEmpty()) { UpdateSubmitToolTag(false); ValidationService->CheckForTagSkips(); ETaskArea ValidateArea = ~ETaskArea::Changelist; for (const FTag* Tag : TagService->GetTagsArray()) { if (Tag->GetValues().Num() != 0) { ValidateArea = ETaskArea::Everything; break; } } ValidationService->QueueByArea(ValidateArea); } FileRefreshedCallback.Broadcast(); } else { UE_LOG(LogSubmitTool, Error, TEXT("Couldn't retrieve information for CL %s"), *ChangelistService->GetCLID()); ChangeState(ESubmitToolAppState::Errored); } } } void FModelInterface::RevertUnchangedAndSubmit() { bool bHasEditOrAdd = false; for (const FSourceControlStateRef& file : ChangelistService->GetFilesInCL()) { bHasEditOrAdd |= file->IsAdded() || file->IsCheckedOut(); } if (bHasEditOrAdd) { ChangelistService->RevertUnchangedFilesAsync(RevertUnchangedCallback); } else { Submit(); } } void FModelInterface::Submit() { if (PresubmitOperationsService->AreTasksPendingQueue()) { // This will call submit again when it's done PresubmitOperationsService->CheckForTagSkips(); if (PresubmitOperationsService->QueueAll()) { return; } } auto AddendumAccumulator = [](const TArray& InAddendums, const FString& InDescription, FString& InOutAccumulated) { for (const FString& Str : InAddendums) { if (!InDescription.Contains(Str, ESearchCase::IgnoreCase)) { InOutAccumulated += (TEXT("\n") + Str); } } }; const FString& CLDescription = GetCLDescription(); FString Addendums; AddendumAccumulator(ValidationService->GetAddendums(), CLDescription, Addendums); AddendumAccumulator(PresubmitOperationsService->GetAddendums(), CLDescription, Addendums); ChangelistService->Submit(Addendums, SubmitFinishedCallback); } void FModelInterface::OnDeleteShelveOperationComplete(const FSourceControlOperationRef& Operation, ECommandResult::Type Result) { if (SubmitToolState == ESubmitToolAppState::Submitting) { if (Result == ECommandResult::Succeeded) { RevertUnchangedAndSubmit(); } else if (Result == ECommandResult::Failed) { ChangeState(ESubmitToolAppState::WaitingUserInput); } else if (Result == ECommandResult::Cancelled) { ChangeState(ESubmitToolAppState::WaitingUserInput); } } else { ChangeState(ESubmitToolAppState::WaitingUserInput); } } void FModelInterface::OnRevertUnchangedOperationComplete(const FSourceControlOperationRef& Operation, ECommandResult::Type Result) { if (SubmitToolState == ESubmitToolAppState::Submitting) { // Revert Unchanged returns as failed if there were no files to revert, check ErrorMessages to see actual failures if (Result == ECommandResult::Cancelled) { ChangeState(ESubmitToolAppState::WaitingUserInput); } else if (Result == ECommandResult::Succeeded || Operation->GetResultInfo().ErrorMessages.Num() == 0) { Submit(); } else if (Result == ECommandResult::Failed) { ChangeState(ESubmitToolAppState::WaitingUserInput); } } else { ChangeState(ESubmitToolAppState::WaitingUserInput); } } void FModelInterface::OnPresubmitOperationsComplete(bool bInSuccess) { if (bInSuccess) { Submit(); } else { UE_LOG(LogSubmitTool, Warning, TEXT("Presubmit operations have failed, submission is not possible, please fix errors and try again.")); PresubmitOperationsService->ResetStates(); } } void FModelInterface::OnSubmitOperationComplete(const FSourceControlOperationRef& Operation, ECommandResult::Type Result) { if (Result == ECommandResult::Succeeded) { // We've submitted, or tried to submit and failed so we only let the user close the app ChangeState(ESubmitToolAppState::Finished); if(FSubmitToolUserPrefs::Get()->bOpenJiraOnSubmit) { TSharedRef CheckIn = StaticCastSharedRef(Operation); const FString CLString = CheckIn->GetSuccessMessage().ToString().Replace(TEXT("Submitted changelist "), TEXT(""), ESearchCase::IgnoreCase).TrimStartAndEnd(); if (CLString.IsNumeric()) { FPlatformApplicationMisc::ClipboardCopy(*CLString); UE_LOG(LogSubmitToolP4, Log, TEXT("Submitted CL copied to clipboard: %s"), *CLString); } FTag* JiraTag = TagService->GetTagOfType(TEXT("JiraIssue")); if (JiraTag != nullptr && JiraTag->GetValues().Num() != 0) { for (const FString& JiraValue : JiraTag->GetValues()) { if (!JiraValue.Equals(TEXT("none"), ESearchCase::IgnoreCase) && !JiraValue.Equals(TEXT("nojira"), ESearchCase::IgnoreCase)) { FString Url = FString::Printf(TEXT("https://%s/browse/%s}"), *Parameters.JiraParameters.ServerAddress, *JiraValue); FPlatformProcess::LaunchURL(*Url, nullptr, nullptr); } } } } if (FSubmitToolUserPrefs::Get()->bCloseOnSubmit) { MainTab.Pin()->RequestCloseTab(); } } else { ChangeState(ESubmitToolAppState::WaitingUserInput); } } bool FModelInterface::Tick(float InDeltaTime) { switch (SubmitToolState) { case ESubmitToolAppState::WaitingUserInput: if (SwarmService->IsRequestRunning() || JiraService->IsBlockingRequestRunning()) { ChangeState(ESubmitToolAppState::P4BlockingOperation); } break; case ESubmitToolAppState::P4BlockingOperation: if (!SwarmService->IsRequestRunning() && !ChangelistService->IsP4OperationRunning() && !JiraService->IsBlockingRequestRunning()) { ChangeState(ESubmitToolAppState::WaitingUserInput); } break; default: break; } return true; } void FModelInterface::ChangeState(ESubmitToolAppState newState, bool bForce) { ensure(IsInGameThread()); if (!IsInGameThread()) { AsyncTask(ENamedThreads::GameThread, [newState, bForce] { ChangeState(newState,bForce); }); return; } const ESubmitToolAppState CurrentState = SubmitToolState; if (bForce) { UE_LOG(LogSubmitToolDebug, Log, TEXT("Transitioned state from '%s' to '%s'"), *StaticEnum()->GetNameStringByValue(static_cast(SubmitToolState)), *StaticEnum()->GetNameStringByValue(static_cast(newState))); SubmitToolState = newState; OnStateChanged.Broadcast(CurrentState, SubmitToolState); } else { if (SubmitToolAppState::AllowedTransitions.Contains(SubmitToolState)) { const TArray& allowedStates = SubmitToolAppState::AllowedTransitions[SubmitToolState]; if (allowedStates.Contains(newState)) { UE_LOG(LogSubmitToolDebug, Log, TEXT("Transitioned state from '%s' to '%s'"), *StaticEnum()->GetNameStringByValue(static_cast(SubmitToolState)), *StaticEnum()->GetNameStringByValue(static_cast(newState))); SubmitToolState = newState; OnStateChanged.Broadcast(CurrentState, SubmitToolState); } else { UE_LOG(LogSubmitToolDebug, Warning, TEXT("Invalid state transition requested from '%s' to '%s'"), *StaticEnum()->GetNameStringByValue(static_cast(SubmitToolState)), *StaticEnum()->GetNameStringByValue(static_cast(newState))); } } else { UE_LOG(LogSubmitToolDebug, Warning, TEXT("Transition not allowed from '%s' to '%s'"), *StaticEnum()->GetNameStringByValue(static_cast(SubmitToolState)), *StaticEnum()->GetNameStringByValue(static_cast(newState))); } } } void FModelInterface::OnGetUsersFromSwarmCompleted(const TUniquePtr& InReview, const FString& InErrorMessage) { if (!InReview.IsValid()) { UE_LOG(LogSubmitTool, Log, TEXT("Could not retrieve swarm review for current changelist. %s"), *InErrorMessage); return; } TArray TargetTags; for (const FTag* Tag : TagService->GetTagsArray()) { if (Tag->Definition.InputSubType.Equals(TEXT("SwarmApproved"), ESearchCase::IgnoreCase)) { TargetTags.Add(Tag); } } if (!TargetTags.IsEmpty()) { TArray SwarmUserValues; for (const TPair& Participant : InReview->Participants) { if (Participant.Key.Equals(InReview->Author, ESearchCase::IgnoreCase)) { continue; } if (Participant.Value.Vote.Value == 1) { if (!SwarmUserValues.Contains(Participant.Key) && !SwarmUserValues.Contains(TEXT("@") + Participant.Key)) { SwarmUserValues.Add(Participant.Key); } } } if (!SwarmUserValues.IsEmpty()) { bool bApplied = false; for (const FTag* Tag : TargetTags) { if (Tag->GetValues() != SwarmUserValues) { SetTagValues(*Tag, SwarmUserValues); bApplied = true; } } if (bApplied) { UE_LOG(LogSubmitTool, Log, TEXT("RB tag set to users that upvoted review '%d' Users: %s"), InReview->Id, *FString::Join(SwarmUserValues, TEXT(", "))); UE_LOG(LogSubmitToolDebug, Log, TEXT("Re-running Tag validator after applying the #rb from swarm")); ValidateCLDescription(); } } } } void FModelInterface::OnSwarmCreateCompleted(bool InResult, const FString& InErrorMessage) { if (InResult) { OnGetUsersFromSwarmCompleted(SwarmService->GetReview(), InErrorMessage); ShowSwarmReview(); } }