Files
UnrealEngine/Engine/Source/Programs/SubmitTool/Private/Models/ModelInterface.cpp
2025-05-18 13:04:45 +08:00

1044 lines
35 KiB
C++

// 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<FSubmitToolServiceProvider>();
// Initialize services
if (Parameters.GeneralParameters.CacheFile.IsEmpty())
{
ServiceProvider->RegisterService<ICacheDataService>(MakeShared<FNoOpCacheDataService>());
}
else
{
ServiceProvider->RegisterService<ICacheDataService>(MakeShared<FCacheDataService>(Parameters.GeneralParameters));
}
SourceControlService = MakeShared<FSubmitToolPerforce>(InParameters);
ServiceProvider->RegisterService<ISTSourceControlService>(SourceControlService.ToSharedRef());
ValidationService = MakeShared<FTasksService>(InParameters.Validators, TEXT("SubmitTool.StandAlone.Validator"));
ServiceProvider->RegisterService<FTasksService>(ValidationService.ToSharedRef(), TEXT("ValidationService"));
PresubmitOperationsService = MakeShared<FTasksService>(InParameters.PresubmitOperations, TEXT("SubmitTool.StandAlone.PresubmitOperation"));
ServiceProvider->RegisterService<FTasksService>(PresubmitOperationsService.ToSharedRef(), TEXT("PresubmitOperationsService"));
CredentialsService = MakeShared<FCredentialsService>(InParameters.OAuthParameters);
ServiceProvider->RegisterService<FCredentialsService>(CredentialsService.ToSharedRef());
ChangelistService = MakeShared<FChangelistService>(InParameters.GeneralParameters, SourceControlService, CLReadyCallback, CLRefreshCallback);
ServiceProvider->RegisterService<FChangelistService>(ChangelistService.ToSharedRef());
P4LockdownService = MakeShared<FP4LockdownService>(InParameters.P4LockdownParameters, ServiceProvider);
ServiceProvider->RegisterService<FP4LockdownService>(P4LockdownService.ToSharedRef());
TagService = MakeShared<FTagService>(InParameters, ChangelistService);
ServiceProvider->RegisterService<FTagService>(TagService.ToSharedRef());
SwarmService = MakeShared<FSwarmService>(ServiceProvider);
ServiceProvider->RegisterService<FSwarmService>(SwarmService.ToSharedRef());
PreflightService = MakeShared<FPreflightService>(InParameters.HordeParameters, this, ServiceProvider);
ServiceProvider->RegisterService<FPreflightService>(PreflightService.ToSharedRef());
JiraService = MakeShared<FJiraService>(InParameters.JiraParameters, 256, ServiceProvider);
ServiceProvider->RegisterService<FJiraService>(JiraService.ToSharedRef());
FNIntegrationService = MakeShared<FIntegrationService>(InParameters.IntegrationParameters, ServiceProvider);
ServiceProvider->RegisterService<FIntegrationService>(FNIntegrationService.ToSharedRef());
UpdateService = MakeShared<FUpdateService>(InParameters.HordeParameters, InParameters.AutoUpdateParameters, ServiceProvider);
ServiceProvider->RegisterService<FUpdateService>(UpdateService.ToSharedRef());
ParseValidators();
ParsePreSubmitOperations();
OnValidationStateUpdatedHandle = ValidationService->OnTasksRunResultUpdated.Add(FOnTaskRunStateChanged::FDelegate::CreateLambda([this](bool bIsValid) {
if (bIsValid)
{
bool bOptionalFailures = false;
for (const TWeakPtr<const FValidatorBase>& 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<const FValidatorBase>& 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<FString, TSharedPtr<FIntegrationOptionBase>>& IntegrationOption : ServiceProvider->GetService<FIntegrationService>()->GetIntegrationOptions())
{
FString Value;
if (IntegrationOption.Value->GetJiraValue(Value) && !Value.IsEmpty())
{
ServiceProvider->GetService<ICacheDataService>()->SetIntegrationFieldValue(GetCLID(), IntegrationOption.Key, Value);
}
}
ServiceProvider->GetService<ICacheDataService>()->SaveCacheToDisk();
ValidationService->StopTasks();
}
void FModelInterface::ParseValidators() const
{
TArray<TSharedRef<FValidatorBase>> Tasks;
for (const TPair<FName, FString> 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<FTagValidator>(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value));
}
else if (TaskDefinition.Type.Equals(SubmitToolParseConstants::UBTValidator, ESearchCase::IgnoreCase))
{
Tasks.Add(MakeShared<FUBTValidator>(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value));
}
else if (TaskDefinition.Type.Equals(SubmitToolParseConstants::EditorValidator, ESearchCase::IgnoreCase))
{
Tasks.Add(MakeShared<FEditorCommandletValidator>(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value));
}
else if (TaskDefinition.Type.Equals(SubmitToolParseConstants::CustomValidator, ESearchCase::IgnoreCase))
{
Tasks.Add(MakeShared<FValidatorRunExecutable>(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value));
}
else if (TaskDefinition.Type.Equals(SubmitToolParseConstants::CrossChangelistValidator, ESearchCase::IgnoreCase))
{
Tasks.Add(MakeShared<FCrossChangelistValidator>(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value));
}
else if (TaskDefinition.Type.Equals(SubmitToolParseConstants::PreflightValidator, ESearchCase::IgnoreCase))
{
Tasks.Add(MakeShared<FPreflightValidator>(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value));
}
else if (TaskDefinition.Type.Equals(SubmitToolParseConstants::PackageDataValidator, ESearchCase::IgnoreCase))
{
Tasks.Add(MakeShared<FPackageDataValidator>(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value));
}
else if (TaskDefinition.Type.Equals(SubmitToolParseConstants::ShaderValidator, ESearchCase::IgnoreCase))
{
Tasks.Add(MakeShared<FShaderValidator>(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value));
}
else if (TaskDefinition.Type.Equals(SubmitToolParseConstants::JsonValidator, ESearchCase::IgnoreCase))
{
Tasks.Add(MakeShared<FJsonValidator>(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<TSharedRef<FValidatorBase>> Tasks;
for (const TPair<FName, FString> 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<FValidatorRunExecutable>(DefinitionPair.Key, Parameters, ServiceProvider.ToSharedRef(), DefinitionPair.Value));
}
else if (TaskDefinition.Type.Equals(SubmitToolParseConstants::VirtualizationToolOp, ESearchCase::IgnoreCase))
{
Tasks.Add(MakeShared<FVirtualizationOperation>(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<const FValidatorBase>& Validator : ValidationService->GetTasks())
{
if (Validator.IsValid())
{
const TSharedPtr<const FValidatorBase> 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<FString>& 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<FString>& FilesInCL = ChangelistService->GetFilesDepotPaths();
FConfiguration::AddOrUpdateEntry(TEXT("$(StreamRoot)"), *GetRootStreamName());
for (const FIncompatibleFilesGroup& FileGroup : Parameters.IncompatibleFilesParams.IncompatibleFileGroups)
{
TArray<size_t, TInlineAllocator<8>> 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<FString>& ShelvedFiles = ChangelistService->GetShelvedFilesDepotPaths();
const TArray<FString>& LocalFiles = ChangelistService->GetFilesDepotPaths();
EDialogFactoryResult DialogResult;
if (ShelvedFiles != LocalFiles)
{
const FText TextTitle = NSLOCTEXT("SourceControl.SubmitWindow", "ShelveConflictTitle", "Shelve - Local conflict");
const size_t MaxFilesToList = 5;
TArray<FString> ShelvedList;
TArray<FString> 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<FString>{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<FSourceControlStateRef> LocalFiles = ChangelistService->GetFilesInCL();
if (LocalFiles.IsEmpty())
{
const TArray<FSourceControlStateRef>& 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<FSCCStream*>& 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<FString>& 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<FCheckIn> CheckIn = StaticCastSharedRef<FCheckIn>(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<ESubmitToolAppState>()->GetNameStringByValue(static_cast<int64>(SubmitToolState)), *StaticEnum<ESubmitToolAppState>()->GetNameStringByValue(static_cast<int64>(newState)));
SubmitToolState = newState;
OnStateChanged.Broadcast(CurrentState, SubmitToolState);
}
else
{
if (SubmitToolAppState::AllowedTransitions.Contains(SubmitToolState))
{
const TArray<ESubmitToolAppState>& allowedStates = SubmitToolAppState::AllowedTransitions[SubmitToolState];
if (allowedStates.Contains(newState))
{
UE_LOG(LogSubmitToolDebug, Log, TEXT("Transitioned state from '%s' to '%s'"), *StaticEnum<ESubmitToolAppState>()->GetNameStringByValue(static_cast<int64>(SubmitToolState)), *StaticEnum<ESubmitToolAppState>()->GetNameStringByValue(static_cast<int64>(newState)));
SubmitToolState = newState;
OnStateChanged.Broadcast(CurrentState, SubmitToolState);
}
else
{
UE_LOG(LogSubmitToolDebug, Warning, TEXT("Invalid state transition requested from '%s' to '%s'"), *StaticEnum<ESubmitToolAppState>()->GetNameStringByValue(static_cast<int64>(SubmitToolState)), *StaticEnum<ESubmitToolAppState>()->GetNameStringByValue(static_cast<int64>(newState)));
}
}
else
{
UE_LOG(LogSubmitToolDebug, Warning, TEXT("Transition not allowed from '%s' to '%s'"), *StaticEnum<ESubmitToolAppState>()->GetNameStringByValue(static_cast<int64>(SubmitToolState)), *StaticEnum<ESubmitToolAppState>()->GetNameStringByValue(static_cast<int64>(newState)));
}
}
}
void FModelInterface::OnGetUsersFromSwarmCompleted(const TUniquePtr<FSwarmReview>& InReview, const FString& InErrorMessage)
{
if (!InReview.IsValid())
{
UE_LOG(LogSubmitTool, Log, TEXT("Could not retrieve swarm review for current changelist. %s"), *InErrorMessage);
return;
}
TArray<const FTag*> TargetTags;
for (const FTag* Tag : TagService->GetTagsArray())
{
if (Tag->Definition.InputSubType.Equals(TEXT("SwarmApproved"), ESearchCase::IgnoreCase))
{
TargetTags.Add(Tag);
}
}
if (!TargetTags.IsEmpty())
{
TArray<FString> SwarmUserValues;
for (const TPair<FString, FSwarmReviewParticipant>& 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();
}
}