// Copyright Epic Games, Inc. All Rights Reserved. #include "CommandBase.h" #include "HAL/FileManager.h" #include "ISourceControlModule.h" #include "ISourceControlProvider.h" #include "Misc/PackageName.h" #include "Misc/Paths.h" #include "Misc/ScopeExit.h" #include "SourceControlInitSettings.h" #include "SourceControlOperations.h" #include "UnrealVirtualizationTool.h" namespace UE::Virtualization { /** Utility for testing if a file path resolves to a valid package file or not */ bool IsPackageFile(const FString FilePath) { // ::IsPackageExtension requires a TCHAR so we cannot use FPathViews here const FString Extension = FPaths::GetExtension(FilePath); // Currently we don't virtualize text based assets so no call to FPackageName::IsTextPackageExtension return FPackageName::IsPackageExtension(*Extension); } FCommand::FCommand(FStringView InCommandName) : CommandName(InCommandName) { } FCommand::~FCommand() { } void FCommand::ParseCommandLine(const TCHAR* CmdLine, TArray& Tokens, TArray& Switches) { // TODO: Taken from UCommandlet, maybe consider moving this code to a general purpose utility. We // could also make a version that returns TArray FString NextToken; while (FParse::Token(CmdLine, NextToken, false)) { if (**NextToken == TCHAR('-')) { new(Switches) FString(NextToken.Mid(1)); } else { new(Tokens) FString(NextToken); } } } FCommand::EPathResult FCommand::ParseSwitchForPaths(const FString& Switch, TArray& OutPackages) { FString Path; if (FParse::Value(*Switch, TEXT("Package="), Path)) { FPaths::NormalizeFilename(Path); if (!FPackageName::IsPackageFilename(Path)) { UE_LOG(LogVirtualizationTool, Error, TEXT("Requested package file '%s' is not a valid package filename"), *Path); return EPathResult::Error; } if (!IFileManager::Get().FileExists(*Path)) { UE_LOG(LogVirtualizationTool, Error, TEXT("Could not find the requested package file '%s'"), *Path); return EPathResult::Error; } OutPackages.Add(Path); return EPathResult::Success; } else if (FParse::Value(*Switch, TEXT("PackageDir="), Path) || FParse::Value(*Switch, TEXT("PackageFolder="), Path)) { // Note that 'PackageFolder' is the switch used by the resave commandlet, so allowing it here for compatibility purposes FPaths::NormalizeFilename(Path); if (IFileManager::Get().DirectoryExists(*Path)) { IFileManager::Get().IterateDirectoryRecursively(*Path, [&OutPackages](const TCHAR* Path, bool bIsDirectory) { if (!bIsDirectory && FPackageName::IsPackageFilename(Path)) { FString FilePath(Path); FPaths::NormalizeFilename(FilePath); OutPackages.Add(MoveTemp(FilePath)); } return true; // Continue }); return EPathResult::Success; } else { UE_LOG(LogVirtualizationTool, Error, TEXT("Could not find the requested directory '%s'"), *Path); return EPathResult::Error; } } return EPathResult::NotFound; } bool FCommand::TryConnectToSourceControl(FStringView ClientSpecName) { if (SCCProvider != nullptr) { // Already connected so just return return true; } UE_LOG(LogVirtualizationTool, Log, TEXT("Trying to connect to source control...")); if (FPaths::IsProjectFilePathSet()) { // If the project has been set then we can use the default source control provider if (!ISourceControlModule::Get().GetProvider().IsEnabled()) { ISourceControlModule::Get().SetProvider(FName("Perforce")); } SCCProvider = &ISourceControlModule::Get().GetProvider(); SCCProvider->Init(true); } else { // If we do not have a project set we need to create our own provider. We make the assumption that // we should use perforce since the only reason we should need a provider without a project is if // we are parsing a perforce changelist for files to operate on. FSourceControlInitSettings SCCSettings(FSourceControlInitSettings::EBehavior::OverrideAll); SCCSettings.SetConfigBehavior(FSourceControlInitSettings::EConfigBehavior::ReadOnly); SCCSettings.SetCmdLineFlags(FSourceControlInitSettings::ECmdLineFlags::ReadAll); SCCSettings.AddSetting(TEXT("P4Client"), ClientSpecName); OwnedSCCProvider = ISourceControlModule::Get().CreateProvider(FName("Perforce"), TEXT("UnrealVirtualizationTool"), SCCSettings); if (OwnedSCCProvider.IsValid()) { SCCProvider = OwnedSCCProvider.Get(); SCCProvider->Init(true); } else { UE_LOG(LogVirtualizationTool, Error, TEXT("\tFailed to instantiate a perforce revision control connection")); return false; } } if (SCCProvider->IsAvailable()) { TMap StatusMap = SCCProvider->GetStatus(); if (FString* Server = StatusMap.Find(ISourceControlProvider::EStatus::Port)) { UE_LOG(LogVirtualizationTool, Display, TEXT("\tSuccessfully connected to server '%s'"), **Server); } return true; } else { UE_LOG(LogVirtualizationTool, Error, TEXT("\tFailed to establish a perforce connection")); return false; } } bool FCommand::TryParseChangelist(FStringView ClientSpecName, FStringView ChangelistNumber, TArray& OutPackages, FSourceControlChangelistPtr* OutChangelist) { TRACE_CPUPROFILER_EVENT_SCOPE(TryParseChangelist); UE_LOG(LogVirtualizationTool, Display, TEXT("\tAttempting to parse changelist '%.*s' in workspace '%.*s'"), ChangelistNumber.Len(), ChangelistNumber.GetData(), ClientSpecName.Len(), ClientSpecName.GetData()); if (!TryConnectToSourceControl(ClientSpecName)) { return false; } if (SCCProvider == nullptr) { UE_LOG(LogVirtualizationTool, Error, TEXT("No valid source control connection found!")); return false; } if (!SCCProvider->UsesChangelists()) { UE_LOG(LogVirtualizationTool, Error, TEXT("The source control provider does not support the use of changelists")); return false; } TArray Changelists = SCCProvider->GetChangelists(EStateCacheUsage::ForceUpdate); if (Changelists.IsEmpty()) { UE_LOG(LogVirtualizationTool, Error, TEXT("Failed to find any changelists")); return false; } // TODO: At the moment we have to poll for all changelists under the current workspace and then iterate over // them to find the one we want. // We need to add better support in the API to go from a changelist number to a FSourceControlChangelistRef // directly. TArray ChangelistsStates; if (SCCProvider->GetState(Changelists, ChangelistsStates, EStateCacheUsage::Use) != ECommandResult::Succeeded) { UE_LOG(LogVirtualizationTool, Error, TEXT("Failed to find changelist data")); return false; } for (FSourceControlChangelistStateRef& ChangelistState : ChangelistsStates) { const FText DisplayText = ChangelistState->GetDisplayText(); if (ChangelistNumber == DisplayText.ToString()) { TSharedRef Operation = ISourceControlOperation::Create(); FSourceControlChangelistRef Changelist = ChangelistState->GetChangelist(); Operation->SetChangelistsToUpdate(MakeArrayView(&Changelist, 1)); Operation->SetUpdateFilesStates(true); if (SCCProvider->Execute(Operation, EConcurrency::Synchronous) != ECommandResult::Succeeded) { UE_LOG(LogVirtualizationTool, Error, TEXT("Failed to find the files in changelist '%.*s'"), ChangelistNumber.Len(), ChangelistNumber.GetData()); return false; } const TArray& FilesinChangelist = ChangelistState->GetFilesStates(); UE_LOG(LogVirtualizationTool, Log, TEXT("\tFound %d files in the changelist"), FilesinChangelist.Num()); for (const FSourceControlStateRef& FileState : FilesinChangelist) { if (!IsPackageFile(FileState->GetFilename())) { UE_LOG(LogVirtualizationTool, Log, TEXT("\tIgnoring non-package file '%s'"), *FileState->GetFilename()); continue; } if (FileState->IsDeleted()) { UE_LOG(LogVirtualizationTool, Verbose, TEXT("\tIgnoring package marked for delete '%s'"), *FileState->GetFilename()); continue; } if (FileState->IsIgnored()) { UE_LOG(LogVirtualizationTool, Verbose, TEXT("\tIgnoring package marked for ignore '%s'"), *FileState->GetFilename()); continue; } OutPackages.Add(FileState->GetFilename()); } if (OutChangelist != nullptr) { *OutChangelist = Changelist; } return true; } } UE_LOG(LogVirtualizationTool, Error, TEXT("Failed to find the changelist '%.*s'"), ChangelistNumber.Len(), ChangelistNumber.GetData()); return false; } FString FCommand::FindClientSpecForChangelist(FStringView ChangelistNumber) { TRACE_CPUPROFILER_EVENT_SCOPE(FindClientSpecForChangelist); UE_LOG(LogVirtualizationTool, Display, TEXT("\tAttempting to find the workspace for '%.*s'"), ChangelistNumber.Len(), ChangelistNumber.GetData()); if (!TryConnectToSourceControl()) { return FString(); } if (SCCProvider == nullptr) { UE_LOG(LogVirtualizationTool, Error, TEXT("No valid source control connection found!")); return FString(); } if (!SCCProvider->UsesChangelists()) { UE_LOG(LogVirtualizationTool, Error, TEXT("The source control provider does not support the use of changelists")); return FString(); } TSharedRef Operation = ISourceControlOperation::Create(ChangelistNumber); if (SCCProvider->Execute(Operation, EConcurrency::Synchronous) == ECommandResult::Succeeded && !Operation->GetChangelistDetails().IsEmpty()) { const FString* ClientSpec = Operation->GetChangelistDetails()[0].Find(TEXT("Client")); if (ClientSpec != nullptr) { return *ClientSpec; } else { UE_LOG(LogVirtualizationTool, Error, TEXT("Unable to find the 'client' field for the changelist '%.*s'"), ChangelistNumber.Len(), ChangelistNumber.GetData()); return FString(); } } else { UE_LOG(LogVirtualizationTool, Error, TEXT("Failed to find details for the changelist '%.*s'"), ChangelistNumber.Len(), ChangelistNumber.GetData()); return FString(); } } } //namespace UE::Virtualization