// Copyright Epic Games, Inc. All Rights Reserved. #include "SourceControlHelpers.h" #include "Algo/Transform.h" #include "AssetRegistry/AssetData.h" #include "ISourceControlState.h" #include "HAL/FileManager.h" #include "Misc/Paths.h" #include "Misc/ConfigContext.h" #include "ISourceControlOperation.h" #include "SourceControlOperations.h" #include "ISourceControlProvider.h" #include "ISourceControlModule.h" #include "ISourceControlLabel.h" #include "UObject/Linker.h" #include "UObject/Package.h" #include "UObject/UObjectIterator.h" #include "Misc/PackageName.h" #include "Logging/MessageLog.h" #include "AssetRegistry/AssetRegistryModule.h" #include "Internationalization/PackageLocalizationUtil.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(SourceControlHelpers) #if WITH_EDITOR #include "Editor.h" #include "PackageTools.h" #include "ObjectTools.h" #include "FileHelpers.h" #endif #define LOCTEXT_NAMESPACE "SourceControlHelpers" namespace SourceControlHelpersInternal { /* * Status info set by LogError() and USourceControlHelpers methods if an error occurs * regardless whether their bSilent is set or not. * Should be empty if there is was no error. * @see USourceControlHelpers::LastErrorMsg(), LogError() */ FText LastErrorText; /* Store error and write to Log if bSilent is false. */ inline void LogError(const FText& ErrorText, bool bSilent) { LastErrorText = ErrorText; if (!bSilent) { FMessageLog("SourceControl").Error(LastErrorText); } } /* Return provider if ready to go, else return nullptr. */ ISourceControlProvider* VerifySourceControl(bool bSilent) { ISourceControlModule& SCModule = ISourceControlModule::Get(); if (!SCModule.IsEnabled()) { LogError(LOCTEXT("SourceControlDisabled", "Revision control is not enabled."), bSilent); return nullptr; } ISourceControlProvider* Provider = &SCModule.GetProvider(); if (!Provider->IsAvailable()) { LogError(LOCTEXT("SourceControlServerUnavailable", "Revision control server is currently not available."), bSilent); return nullptr; } // Clear the last error text if there hasn't been an error (yet). LastErrorText = FText::GetEmpty(); return Provider; } /* * Converts specified file to fully qualified file path that is compatible with source control. * * @param InFile File string - can be either fully qualified path, relative path, long package name, asset path or export text path (often stored on clipboard) * @param bSilent if false then write out any error info to the Log. Any error text can be retrieved by LastErrorMsg() regardless. * @return Fully qualified file path to use with source control or "" if conversion unsuccessful. */ FString ConvertFileToQualifiedPath(const FString& InFile, bool bSilent, bool bAllowDirectories = false, const TCHAR* AssociatedExtension = nullptr) { // Converted to qualified file path FString SCFile; if (InFile.IsEmpty()) { LogError(LOCTEXT("UnspecifiedFile", "File not specified"), bSilent); return SCFile; } // Try to determine if file is one of: // - fully qualified path // - relative path // - long package name // - asset path // - export text path (often stored on clipboard) // // For example: // - D:\Epic\Dev-Ent\Projects\Python3rdBP\Content\Mannequin\Animations\ThirdPersonIdle.uasset // - Content\Mannequin\Animations\ThirdPersonIdle.uasset // - /Game/Mannequin/Animations/ThirdPersonIdle // - /Game/Mannequin/Animations/ThirdPersonIdle.ThirdPersonIdle // - AnimSequence'/Game/Mannequin/Animations/ThirdPersonIdle.ThirdPersonIdle' SCFile = InFile; // Is ExportTextPath (often stored in Clipboard) form? // - i.e. AnimSequence'/Game/Mannequin/Animations/ThirdPersonIdle.ThirdPersonIdle' if (SCFile[SCFile.Len() - 1] == '\'') { SCFile = FPackageName::ExportTextPathToObjectPath(SCFile); } // Package paths if (SCFile[0] == TEXT('/') && FPackageName::IsValidLongPackageName(SCFile, /*bIncludeReadOnlyRoots*/false)) { // Assume it is a package bool bPackage = true; // Try to get filename by finding it on disk if (!FPackageName::DoesPackageExist(SCFile, &SCFile)) { // First do the conversion without any extension set, as this will allow us to test whether the path represents an existing directory rather than an asset if (FPackageName::TryConvertLongPackageNameToFilename(SCFile, SCFile)) { if (bAllowDirectories && FPaths::DirectoryExists(SCFile)) { // This path mapped to a known directory, so ensure it ends in a slash SCFile /= FString(); } else if (AssociatedExtension) { // Just use the requested extension SCFile += AssociatedExtension; } else { // The package does not exist on disk, see if we can find it in memory and predict the file extension UPackage* Package = FindPackage(nullptr, *SCFile); SCFile += (Package && Package->ContainsMap() ? FPackageName::GetMapPackageExtension() : FPackageName::GetAssetPackageExtension()); } } else { bPackage = false; } } if (bPackage) { SCFile = FPaths::ConvertRelativePathToFull(SCFile); return SCFile; } } // Assume it is a qualified or relative file path // Could normalize it //FPaths::NormalizeFilename(SCFile); if (!FPaths::IsRelative(SCFile)) { return SCFile; } // Qualify based on process base directory. // Something akin to "C:/Epic/UE/Engine/Binaries/Win64/" as a current path. SCFile = FPaths::ConvertRelativePathToFull(InFile); if (FPaths::FileExists(SCFile) || (bAllowDirectories && FPaths::DirectoryExists(SCFile))) { return SCFile; } // Qualify based on project directory. SCFile = FPaths::ConvertRelativePathToFull(FPaths::ConvertRelativePathToFull(FPaths::ProjectDir()), InFile); if (FPaths::FileExists(SCFile) || (bAllowDirectories && FPaths::DirectoryExists(SCFile))) { return SCFile; } // Qualify based on Engine directory SCFile = FPaths::ConvertRelativePathToFull(FPaths::ConvertRelativePathToFull(FPaths::EngineDir()), InFile); return SCFile; } /** * Converts specified files to fully qualified file paths that are compatible with source control. * * @param InFiles File strings - can be either fully qualified path, relative path, long package name, asset name or export text path (often stored on clipboard) * @param OutFilePaths Fully qualified file paths to use with source control or "" if conversion unsuccessful. * @param bSilent if false then write out any error info to the Log. Any error text can be retrieved by LastErrorMsg() regardless. * @return true if all files successfully converted, false if any had errors */ bool ConvertFilesToQualifiedPaths(const TArray& InFiles, TArray& OutFilePaths, bool bSilent, bool bAllowDirectories = false) { uint32 SkipNum = 0u; for (const FString& File : InFiles) { FString SCFile = ConvertFileToQualifiedPath(File, bSilent, bAllowDirectories); if (SCFile.IsEmpty()) { SkipNum++; } else { OutFilePaths.Add(MoveTemp(SCFile)); } } if (SkipNum) { FFormatNamedArguments Arguments; Arguments.Add(TEXT("SkipNum"), FText::AsNumber(SkipNum)); LogError(FText::Format(LOCTEXT("FilesSkipped", "During conversion to qualified file paths, {SkipNum} files were skipped!"), Arguments), bSilent); return false; } return true; } void RunQueryFileStates(TConstArrayView InFiles, EConcurrency::Type InConcurrencyType, bool bSilent, TFunction FileStateCallback) { // Ensure source control system is up and running ISourceControlProvider* Provider = SourceControlHelpersInternal::VerifySourceControl(bSilent); if (!Provider) { return; } TArray SCFilesToUpdate; for (const FString& File : InFiles) { // Determine file type and ensure it is in form source control wants FString SCFile = SourceControlHelpersInternal::ConvertFileToQualifiedPath(File, bSilent); if (SCFile.IsEmpty()) { // Improper or invalid SCC state FFormatNamedArguments Arguments; Arguments.Add(TEXT("InFile"), FText::FromString(File)); SourceControlHelpersInternal::LogError(FText::Format(LOCTEXT("CouldNotResolveFilepath", "Could not resolve file path for '{InFile}'."), Arguments), bSilent); continue; } SCFilesToUpdate.Emplace(MoveTemp(SCFile)); } // Make sure we update the modified state of the files (Perforce requires this // since can be a more expensive test). TSharedRef UpdateStatusOperation = ISourceControlOperation::Create(); UpdateStatusOperation->SetUpdateModifiedState(true); ISourceControlModule::Get().GetProvider().Execute( UpdateStatusOperation, SCFilesToUpdate, InConcurrencyType, FSourceControlOperationComplete::CreateLambda([FileStateCallback, Provider, SCFilesToUpdate, bSilent](const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult) { TArray SCStates; Provider->GetState(SCFilesToUpdate, SCStates, EStateCacheUsage::Use); for (const FSourceControlStatePtr SCState : SCStates) { FSourceControlState State; State.SetFromStatus(SCState); FileStateCallback(State); } })); } } // namespace SourceControlHelpersInternal FString USourceControlHelpers::CurrentProvider() { // Note that if there is no provider there is still a dummy default provider object ISourceControlProvider& Provider = ISourceControlModule::Get().GetProvider(); return Provider.GetName().ToString(); } bool USourceControlHelpers::IsEnabled() { return ISourceControlModule::Get().IsEnabled(); } bool USourceControlHelpers::IsAvailable() { ISourceControlModule& SCModule = ISourceControlModule::Get(); return SCModule.IsEnabled() && SCModule.GetProvider().IsAvailable(); } FText USourceControlHelpers::LastErrorMsg() { return SourceControlHelpersInternal::LastErrorText; } bool USourceControlHelpers::SyncFile(const FString& InFile, bool bSilent) { // Determine file type and ensure it is in form source control wants FString SCFile = SourceControlHelpersInternal::ConvertFileToQualifiedPath(InFile, bSilent, /*bAllowDirectories*/true); if (SCFile.IsEmpty()) { return false; } // Ensure source control system is up and running ISourceControlProvider* Provider = SourceControlHelpersInternal::VerifySourceControl(bSilent); if (!Provider) { return false; } if (Provider->Execute(ISourceControlOperation::Create(), SCFile) == ECommandResult::Succeeded) { return true; } // Only error info after this point FFormatNamedArguments Arguments; Arguments.Add(TEXT("InFile"), FText::FromString(InFile)); Arguments.Add(TEXT("SCFile"), FText::FromString(SCFile)); SourceControlHelpersInternal::LogError(FText::Format(LOCTEXT("SyncFailed", "Failed to sync file '{InFile}' ({SCFile})."), Arguments), bSilent); return false; } bool USourceControlHelpers::SyncFiles(const TArray& InFiles, bool bSilent) { // If we have nothing to process, exit immediately if (InFiles.IsEmpty()) { return true; } ISourceControlProvider* Provider = SourceControlHelpersInternal::VerifySourceControl(bSilent); if (!Provider) { return false; } TArray FilePaths; // Even if some files were skipped, still apply to the others bool bFilesSkipped = !SourceControlHelpersInternal::ConvertFilesToQualifiedPaths(InFiles, FilePaths, bSilent, /*bAllowDirectories*/true); // Less error checking and info is made for multiple files than the single file version. // This multi-file version could be made similarly more sophisticated. ECommandResult::Type Result = Provider->Execute(ISourceControlOperation::Create(), FilePaths); return !bFilesSkipped && (Result == ECommandResult::Succeeded); } void LogCheckoutFailure(const FString& InFile, const FString& SCFile, FSourceControlStatePtr SCState, bool bCheckoutFailed, bool bSilent) { FString SimultaneousCheckoutUser; FFormatNamedArguments Arguments; Arguments.Add(TEXT("InFile"), FText::FromString(InFile)); Arguments.Add(TEXT("SCFile"), FText::FromString(SCFile)); if (bCheckoutFailed) { SourceControlHelpersInternal::LogError(FText::Format(LOCTEXT("CheckoutFailed", "Failed to check out file '{InFile}' ({SCFile})."), Arguments), bSilent); } else if (!SCState->IsSourceControlled()) { SourceControlHelpersInternal::LogError(FText::Format(LOCTEXT("NotSourceControlled", "Could not check out the file '{InFile}' because it is not under revision control ({SCFile})."), Arguments), bSilent); } else if (!SCState->IsCurrent()) { SourceControlHelpersInternal::LogError(FText::Format(LOCTEXT("NotAtHeadRevision", "File '{InFile}' is not at head revision ({SCFile})."), Arguments), bSilent); } else if (SCState->IsCheckedOutOther(&(SimultaneousCheckoutUser))) { Arguments.Add(TEXT("SimultaneousCheckoutUser"), FText::FromString(SimultaneousCheckoutUser)); SourceControlHelpersInternal::LogError(FText::Format(LOCTEXT("SimultaneousCheckout", "File '{InFile}' is checked out by another ({SimultaneousCheckoutUser}) ({SCFile})."), Arguments), bSilent); } else { // Improper or invalid SCC state SourceControlHelpersInternal::LogError(FText::Format(LOCTEXT("CouldNotDetermineState", "Could not determine revision control state of file '{InFile}' ({SCFile})."), Arguments), bSilent); } } bool USourceControlHelpers::CheckOutFile(const FString& InFile, bool bSilent) { // Determine file type and ensure it is in form source control wants FString SCFile = SourceControlHelpersInternal::ConvertFileToQualifiedPath(InFile, bSilent); if (SCFile.IsEmpty()) { return false; } // Ensure source control system is up and running ISourceControlProvider* Provider = SourceControlHelpersInternal::VerifySourceControl(bSilent); if (!Provider) { return false; } FSourceControlStatePtr SCState = Provider->GetState(SCFile, EStateCacheUsage::ForceUpdate); if (!SCState.IsValid()) { LogCheckoutFailure(InFile, SCFile, SCState, false, bSilent); return false; } if (SCState->IsCheckedOut() || SCState->IsAdded()) { // Already checked out or opened for add return true; } bool bCheckOutFailed = false; if (SCState->CanCheckout()) { if (Provider->Execute(ISourceControlOperation::Create(), SCFile) == ECommandResult::Succeeded) { return true; } bCheckOutFailed = true; } LogCheckoutFailure(InFile, SCFile, SCState, bCheckOutFailed, bSilent); return false; } bool USourceControlHelpers::CheckOutFiles(const TArray& InFiles, bool bSilent) { // If we have nothing to process, exit immediately if (InFiles.IsEmpty()) { return true; } // Determine file type and ensure it is in form source control wants // Even if some files were skipped, still apply to the others TArray SCFiles; bool bFilesSkipped = !SourceControlHelpersInternal::ConvertFilesToQualifiedPaths(InFiles, SCFiles, bSilent); const int32 NumFiles = SCFiles.Num(); // Ensure source control system is up and running ISourceControlProvider* Provider = SourceControlHelpersInternal::VerifySourceControl(bSilent); if (!Provider) { // Error or can't communicate with source control return false; } TArray SCStates; Provider->GetState(SCFiles, SCStates, EStateCacheUsage::ForceUpdate); TArray SCFilesToCheckout; bool bCannotCheckoutAtLeastOneFile = false; for (int32 Index = 0; Index < NumFiles; ++Index) { FString SCFile = SCFiles[Index]; FSourceControlStateRef SCState = SCStates[Index]; // Less error checking and info is made for multiple files than the single file version. // This multi-file version could be made similarly more sophisticated. if (!SCState->IsCheckedOut() && !SCState->IsAdded()) { if (SCState->CanCheckout()) { SCFilesToCheckout.Add(SCFile); } else { bCannotCheckoutAtLeastOneFile = true; LogCheckoutFailure(InFiles[Index], SCFile, SCState, false, bSilent); } } } bool bSuccess = !bFilesSkipped && !bCannotCheckoutAtLeastOneFile; if (bSuccess && SCFilesToCheckout.Num()) { bSuccess = Provider->Execute(ISourceControlOperation::Create(), SCFilesToCheckout) == ECommandResult::Succeeded; } return bSuccess; } bool USourceControlHelpers::CheckOutOrAddFile(const FString& InFile, bool bSilent) { // Determine file type and ensure it is in form source control wants FString SCFile = SourceControlHelpersInternal::ConvertFileToQualifiedPath(InFile, bSilent); if (SCFile.IsEmpty()) { return false; } // Ensure source control system is up and running ISourceControlProvider* Provider = SourceControlHelpersInternal::VerifySourceControl(bSilent); if (!Provider) { return false; } FSourceControlStatePtr SCState = Provider->GetState(SCFile, EStateCacheUsage::ForceUpdate); if (!SCState.IsValid()) { // Improper or invalid SCC state FFormatNamedArguments Arguments; Arguments.Add(TEXT("InFile"), FText::FromString(InFile)); Arguments.Add(TEXT("SCFile"), FText::FromString(SCFile)); SourceControlHelpersInternal::LogError(FText::Format(LOCTEXT("CouldNotDetermineState", "Could not determine revision control state of file '{InFile}' ({SCFile})."), Arguments), bSilent); return false; } if (SCState->IsCheckedOut() || SCState->IsAdded()) { // Already checked out or opened for add return true; } // Stuff single file in array for functions that require array TArray FilesToBeCheckedOut; FilesToBeCheckedOut.Add(SCFile); if (SCState->CanCheckout()) { if (Provider->Execute(ISourceControlOperation::Create(), FilesToBeCheckedOut) != ECommandResult::Succeeded) { FFormatNamedArguments Arguments; Arguments.Add(TEXT("InFile"), FText::FromString(InFile)); Arguments.Add(TEXT("SCFile"), FText::FromString(SCFile)); SourceControlHelpersInternal::LogError(FText::Format(LOCTEXT("CheckoutFailed", "Failed to check out file '{InFile}' ({SCFile})."), Arguments), bSilent); return false; } return true; } bool bAddFail = false; if (!SCState->IsSourceControlled()) { if (Provider->Execute(ISourceControlOperation::Create(), FilesToBeCheckedOut) == ECommandResult::Succeeded) { return true; } bAddFail = true;; } FString SimultaneousCheckoutUser; FFormatNamedArguments Arguments; Arguments.Add(TEXT("InFile"), FText::FromString(InFile)); Arguments.Add(TEXT("SCFile"), FText::FromString(SCFile)); if (bAddFail) { SourceControlHelpersInternal::LogError(FText::Format(LOCTEXT("AddFailed", "Failed to add file '{InFile}' to revision control ({SCFile})."), Arguments), bSilent); } else if (!SCState->IsCurrent()) { SourceControlHelpersInternal::LogError(FText::Format(LOCTEXT("NotAtHeadRevision", "File '{InFile}' is not at head revision ({SCFile})."), Arguments), bSilent); } else if (SCState->IsCheckedOutOther(&(SimultaneousCheckoutUser))) { Arguments.Add(TEXT("SimultaneousCheckoutUser"), FText::FromString(SimultaneousCheckoutUser)); SourceControlHelpersInternal::LogError(FText::Format(LOCTEXT("SimultaneousCheckout", "File '{InFile}' is checked out by another ({SimultaneousCheckoutUser}) ({SCFile})."), Arguments), bSilent); } else { // Improper or invalid SCC state SourceControlHelpersInternal::LogError(FText::Format(LOCTEXT("CouldNotDetermineState", "Could not determine revision control state of file '{InFile}' ({SCFile})."), Arguments), bSilent); } return false; } bool USourceControlHelpers::CheckOutOrAddFiles(const TArray& InFiles, bool bSilent) { // If we have nothing to process, exit immediately if (InFiles.IsEmpty()) { return true; } // Determine file type and ensure it is in form source control wants // Even if some files were skipped, still apply to the others TArray SCFiles; bool bFilesSkipped = !SourceControlHelpersInternal::ConvertFilesToQualifiedPaths(InFiles, SCFiles, bSilent); const int32 NumFiles = SCFiles.Num(); // Ensure source control system is up and running ISourceControlProvider* Provider = SourceControlHelpersInternal::VerifySourceControl(bSilent); if (!Provider) { // Error or can't communicate with source control return false; } TArray SCStates; Provider->GetState(SCFiles, SCStates, EStateCacheUsage::ForceUpdate); TArray SCFilesToAdd; TArray SCFilesToCheckout; bool bCannotAddAtLeastOneFile = false; bool bCannotCheckoutAtLeastOneFile = false; for (int32 Index = 0; Index < NumFiles; ++Index) { FString SCFile = SCFiles[Index]; FSourceControlStateRef SCState = SCStates[Index]; // Less error checking and info is made for multiple files than the single file version. // This multi-file version could be made similarly more sophisticated. if (!SCState->IsCheckedOut() && !SCState->IsAdded()) { if (!SCState->IsSourceControlled()) { if (SCState->CanAdd()) { SCFilesToAdd.Add(SCFile); } else { bCannotAddAtLeastOneFile = true; } } else { if (SCState->CanCheckout()) { SCFilesToCheckout.Add(SCFile); } else { bCannotCheckoutAtLeastOneFile = true; } } } } bool bSuccess = !bFilesSkipped && !bCannotCheckoutAtLeastOneFile && !bCannotAddAtLeastOneFile; if (SCFilesToAdd.Num()) { bSuccess &= Provider->Execute(ISourceControlOperation::Create(), SCFilesToAdd) == ECommandResult::Succeeded; } if (SCFilesToCheckout.Num()) { bSuccess &= Provider->Execute(ISourceControlOperation::Create(), SCFilesToCheckout) == ECommandResult::Succeeded; } return bSuccess; } bool USourceControlHelpers::MarkFileForAdd(const FString& InFile, bool bSilent) { // Determine file type and ensure it is in form source control wants FString SCFile = SourceControlHelpersInternal::ConvertFileToQualifiedPath(InFile, bSilent); if (SCFile.IsEmpty()) { return false; } // Ensure source control system is up and running ISourceControlProvider* Provider = SourceControlHelpersInternal::VerifySourceControl(bSilent); if (!Provider) { return false; } // Mark for add now if needed FSourceControlStatePtr SCState = Provider->GetState(SCFile, EStateCacheUsage::Use); if (!SCState.IsValid()) { // Improper or invalid SCC state FFormatNamedArguments Arguments; Arguments.Add(TEXT("InFile"), FText::FromString(InFile)); Arguments.Add(TEXT("SCFile"), FText::FromString(SCFile)); SourceControlHelpersInternal::LogError(FText::Format(LOCTEXT("CouldNotDetermineState", "Could not determine revision control state of file '{InFile}' ({SCFile})."), Arguments), bSilent); return false; } // Add if necessary if (SCState->IsUnknown() || (!SCState->IsSourceControlled() && !SCState->IsAdded())) { if (Provider->Execute(ISourceControlOperation::Create(), SCFile) != ECommandResult::Succeeded) { FFormatNamedArguments Arguments; Arguments.Add(TEXT("InFile"), FText::FromString(InFile)); Arguments.Add(TEXT("SCFile"), FText::FromString(SCFile)); SourceControlHelpersInternal::LogError(FText::Format(LOCTEXT("MarkForAddFailed", "Failed to add file '{InFile}' to revision control ({SCFile})."), Arguments), bSilent); return false; } } return true; } bool USourceControlHelpers::MarkFilesForAdd(const TArray& InFiles, bool bSilent) { // If we have nothing to process, exit immediately if (InFiles.IsEmpty()) { return true; } ISourceControlProvider* Provider = SourceControlHelpersInternal::VerifySourceControl(bSilent); if (!Provider) { return false; } TArray FilePaths; // Even if some files were skipped, still apply to the others bool bFilesSkipped = !SourceControlHelpersInternal::ConvertFilesToQualifiedPaths(InFiles, FilePaths, bSilent); // Less error checking and info is made for multiple files than the single file version. // This multi-file version could be made similarly more sophisticated. ECommandResult::Type Result = Provider->Execute(ISourceControlOperation::Create(), FilePaths); return !bFilesSkipped && (Result == ECommandResult::Succeeded); } bool USourceControlHelpers::MarkFileForDelete(const FString& InFile, bool bSilent) { // Determine file type and ensure it is in form source control wants FString SCFile = SourceControlHelpersInternal::ConvertFileToQualifiedPath(InFile, bSilent); if (SCFile.IsEmpty()) { return false; } // Ensure source control system is up and running ISourceControlProvider* Provider = SourceControlHelpersInternal::VerifySourceControl(bSilent); if (!Provider) { // Error or can't communicate with source control // Could erase it anyway, though keeping it for now. return false; } FSourceControlStatePtr SCState = Provider->GetState(SCFile, EStateCacheUsage::ForceUpdate); if (!SCState.IsValid()) { // Improper or invalid SCC state FFormatNamedArguments Arguments; Arguments.Add(TEXT("InFile"), FText::FromString(InFile)); Arguments.Add(TEXT("SCFile"), FText::FromString(SCFile)); SourceControlHelpersInternal::LogError(FText::Format(LOCTEXT("CouldNotDetermineState", "Could not determine revision control state of file '{InFile}' ({SCFile})."), Arguments), bSilent); return false; } if (SCState->IsSourceControlled()) { bool bAdded = SCState->IsAdded(); if (bAdded || SCState->IsCheckedOut()) { if (Provider->Execute(ISourceControlOperation::Create(), SCFile) != ECommandResult::Succeeded) { FFormatNamedArguments Arguments; Arguments.Add(TEXT("InFile"), FText::FromString(InFile)); Arguments.Add(TEXT("SCFile"), FText::FromString(SCFile)); SourceControlHelpersInternal::LogError(FText::Format(LOCTEXT("CouldNotRevert", "Could not revert revision control state of file '{InFile}' ({SCFile})."), Arguments), bSilent); return false; } } if (!bAdded) { // Was previously added to source control so mark it for delete if (Provider->Execute(ISourceControlOperation::Create(), SCFile) != ECommandResult::Succeeded) { FFormatNamedArguments Arguments; Arguments.Add(TEXT("InFile"), FText::FromString(InFile)); Arguments.Add(TEXT("SCFile"), FText::FromString(SCFile)); SourceControlHelpersInternal::LogError(FText::Format(LOCTEXT("CouldNotDelete", "Could not delete file '{InFile}' from revision control ({SCFile})."), Arguments), bSilent); return false; } } } // Delete file if it still exists IFileManager& FileManager = IFileManager::Get(); if (FileManager.FileExists(*SCFile)) { // Just a regular file not tracked by source control so erase it. // Don't bother checking if it exists since Delete doesn't care. return FileManager.Delete(*SCFile, false, true); } else { return true; } } bool USourceControlHelpers::MarkFilesForDelete(const TArray& InFiles, bool bSilent) { // If we have nothing to process, exit immediately if (InFiles.IsEmpty()) { return true; } // Determine file type and ensure it is in form source control wants // Even if some files were skipped, still apply to the others TArray SCFiles; bool bFilesSkipped = !SourceControlHelpersInternal::ConvertFilesToQualifiedPaths(InFiles, SCFiles, bSilent); const int32 NumFiles = SCFiles.Num(); // Ensure source control system is up and running ISourceControlProvider* Provider = SourceControlHelpersInternal::VerifySourceControl(bSilent); if (!Provider) { // Error or can't communicate with source control // Could erase the files anyway, though keeping them for now. return false; } TArray SCStates; Provider->GetState(SCFiles, SCStates, EStateCacheUsage::ForceUpdate); TArray SCFilesToRevert; TArray SCFilesToMarkForDelete; bool bCannotDeleteAtLeastOneFile = false; for (int32 Index = 0; Index < NumFiles; ++Index) { FString SCFile = SCFiles[Index]; FSourceControlStateRef SCState = SCStates[Index]; // Less error checking and info is made for multiple files than the single file version. // This multi-file version could be made similarly more sophisticated. if (SCState->IsSourceControlled()) { bool bAdded = SCState->IsAdded(); if (bAdded || SCState->IsCheckedOut()) { SCFilesToRevert.Add(SCFile); } if (!bAdded) { if (SCState->CanDelete()) { SCFilesToMarkForDelete.Add(SCFile); } else { bCannotDeleteAtLeastOneFile = true; } } } } bool bSuccess = !bFilesSkipped && !bCannotDeleteAtLeastOneFile; if (SCFilesToRevert.Num()) { bSuccess &= Provider->Execute(ISourceControlOperation::Create(), SCFilesToRevert) == ECommandResult::Succeeded; } if (SCFilesToMarkForDelete.Num()) { bSuccess &= Provider->Execute(ISourceControlOperation::Create(), SCFilesToMarkForDelete) == ECommandResult::Succeeded; } // Delete remaining files if they still exist : IFileManager& FileManager = IFileManager::Get(); for (FString SCFile : SCFiles) { if (FileManager.FileExists(*SCFile)) { // Just a regular file not tracked by source control so erase it. // Don't bother checking if it exists since Delete doesn't care. bSuccess &= FileManager.Delete(*SCFile, false, true); } } return bSuccess; } bool USourceControlHelpers::RevertFile(const FString& InFile, bool bSilent) { // Determine file type and ensure it is in form source control wants FString SCFile = SourceControlHelpersInternal::ConvertFileToQualifiedPath(InFile, bSilent); if (SCFile.IsEmpty()) { return false; } // Ensure source control system is up and running ISourceControlProvider* Provider = SourceControlHelpersInternal::VerifySourceControl(bSilent); if (!Provider) { return false; } // Revert file regardless of whether it has had any changes made ECommandResult::Type Result = Provider->Execute(ISourceControlOperation::Create(), SCFile); return Result == ECommandResult::Succeeded; } #if WITH_EDITOR bool USourceControlHelpers::ApplyOperationAndReloadPackages(const TArray& InFilenames, const TFunctionRef&)>& InOperation, bool bReloadWorld, bool bInteractive) { ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); TArray PackageNames; TArray PackageFilenames; TArray FilteredPackages; TArray NotFoundPackages; TArray UnlinkPackages; /** * This is necessary to cover an edge case where the user reverts a deleted Level file. * In that case PackageFilename_Internal() will not be able to resolve to the correct extension * as the file is deleted and the package cannot be inspected */ TMap MapPackageNamesToFilenames; bool bSuccess = false; // Normalize packagenames and filenames for (const FString& Filename : InFilenames) { FString Result; if (FPackageName::TryConvertFilenameToLongPackageName(Filename, Result)) { FString Extension = FPaths::GetExtension(Filename); if (Extension == TEXT("umap")) { MapPackageNamesToFilenames.Add(Result, Filename); } PackageNames.Add(MoveTemp(Result)); } else { PackageNames.Add(Filename); } } // If bReloadWorld=false, remove packages if they are loaded external packages or world // If bReloadWorld=true, include world packages to reload TSet UniqueLoadedPackages; PackageNames.RemoveAll([&FilteredPackages, &UniqueLoadedPackages, &NotFoundPackages, &UnlinkPackages, bReloadWorld](const FString& PackageName) -> bool { UPackage* Package = FindPackage(NULL, *PackageName); if (Package != nullptr) { if (UWorld* World = UWorld::FindWorldInPackage(Package)) { if (!bReloadWorld) { FilteredPackages.Emplace(PackageName); return true; // remove the package } } else if (UObject* Asset = Package->FindAssetInPackage()) { if (Asset->IsPackageExternal()) { const bool bIsAssetPartOfWorld = Asset->GetWorld() && Asset->GetWorld()->GetPackage(); // In case this is an explicit world object/actor reload or just an external object unload/detach it if (bReloadWorld || !bIsAssetPartOfWorld) { // detach linker on the object UnlinkPackages.Emplace(Package); // track its world for reloading - not the object package itself if (bIsAssetPartOfWorld) { UniqueLoadedPackages.Add(Asset->GetWorld()->GetPackage()); } return false; } else { FilteredPackages.Emplace(PackageName); return true; // remove the package } } } UniqueLoadedPackages.Add(Package); } else { NotFoundPackages.Add(PackageName); } return false; // do not remove the package }); if (!FilteredPackages.IsEmpty()) { TStringBuilder<2048> Builder; Builder.Join(FilteredPackages, TEXT(", ")); const FString Packages = Builder.ToString(); UE_LOG(LogSourceControl, Warning, TEXT("This operation could not complete on the following map or external packages, please unload them before retrying : %s"), *Packages); return false; } // Reverting may reintroduce some packages, so we need to ensure they're picked up... if (!NotFoundPackages.IsEmpty() && bReloadWorld) { const FString& ExternalActorsFolderName = FPackagePath::GetExternalActorsFolderName(); const FString& ExternalObjectsFolderName = FPackagePath::GetExternalObjectsFolderName(); TArray ExternalFolderNames; ExternalFolderNames.Add(ExternalActorsFolderName); ExternalFolderNames.Add(ExternalObjectsFolderName); // Gather the ExternalPaths in which we're reintroducing packages... TSet NotFoundExternalPaths; for (const FString& ExternalFolderName : ExternalFolderNames) { for (const FString& PackageName : NotFoundPackages) { int32 Index = PackageName.Find(ExternalFolderName); if (Index != INDEX_NONE) { // Format: //__External__//0/AB/CDEFGHIJKLMNOPQRSTUVWX // Result: //__External__/ Index += ExternalFolderName.Len(); Index += 1; Index = PackageName.Find("/", ESearchCase::IgnoreCase, ESearchDir::FromStart, Index); if (Index != INDEX_NONE) { NotFoundExternalPaths.Add(PackageName.Left(Index)); } } } } // See if any of the loaded levels stores their external actors in any of those paths... for (TObjectIterator LevelIt; LevelIt; ++LevelIt) { ULevel* Level = (*LevelIt); if (Level->IsUsingExternalActors()) { const FString& ExternalActorPath = ULevel::GetExternalActorsPath(Level->GetPackage()); if (NotFoundExternalPaths.Contains(ExternalActorPath)) { UniqueLoadedPackages.Add(Level->GetWorld()->GetPackage()); } } if (Level->IsUsingExternalObjects()) { const TArray ExternalObjectPaths = ULevel::GetExternalObjectsPaths(Level->GetPackage()->GetName()); for (const FString& ExternalObjectPath : ExternalObjectPaths) { if (NotFoundExternalPaths.Contains(ExternalObjectPath)) { UniqueLoadedPackages.Add(Level->GetWorld()->GetPackage()); } } } } } // Prepare the packages to be reverted... TArray LoadedPackages = UniqueLoadedPackages.Array(); // Make sure they are unlinked... UnlinkPackages.Append(LoadedPackages); if (UnlinkPackages.Num() > 0) { TArray LoadedObjects; TArray PendingPackageRequestIds; LoadedObjects.Reserve(UnlinkPackages.Num()); PendingPackageRequestIds.Reserve(UnlinkPackages.Num()); for (UPackage* Package : UnlinkPackages) { LoadedObjects.Emplace(Package); if (!Package->IsFullyLoaded()) { PendingPackageRequestIds.Emplace(LoadPackageAsync(Package->GetName())); } } FlushAsyncLoading(PendingPackageRequestIds); ResetLoaders(LoadedObjects); } for (int32 PackageIndex = 0; PackageIndex < PackageNames.Num(); PackageIndex++) { if (MapPackageNamesToFilenames.Contains(PackageNames[PackageIndex])) { PackageFilenames.Add(MapPackageNamesToFilenames[PackageNames[PackageIndex]]); } else { PackageFilenames.Add(PackageFilename(PackageNames[PackageIndex])); } } // Apply Operation bSuccess = InOperation(PackageFilenames); // Reverting may have deleted some packages, so we need to delete those and unload them rather than re-load them... FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); TArray> ObjectsMissingOnDisk; LoadedPackages.RemoveAll([&](UPackage* InPackage) -> bool { const FString PackageExtension = InPackage->ContainsMap() ? FPackageName::GetMapPackageExtension() : FPackageName::GetAssetPackageExtension(); const FString PackageFilename = FPackageName::LongPackageNameToFilename(InPackage->GetName(), PackageExtension); if (!FPaths::FileExists(PackageFilename)) { TArray Assets; AssetRegistryModule.Get().GetAssetsByPackageName(*InPackage->GetName(), Assets); for (const FAssetData& Asset : Assets) { if (UObject* ObjectToDelete = Asset.FastGetAsset()) { ObjectsMissingOnDisk.Add(ObjectToDelete); } } return true; // remove package } return false; // keep package }); // Hot-reload the new packages... if (LoadedPackages.Num() > 0) { // Split into world and non-world packages and reload them separately. TArray LoadedWorldPackages; TArray LoadedNonWorldPackages; LoadedWorldPackages.Reserve(LoadedPackages.Num()); LoadedNonWorldPackages.Reserve(LoadedPackages.Num()); for (UPackage* Package : LoadedPackages) { if (UWorld::FindWorldInPackage(Package)) { LoadedWorldPackages.Add(Package); } else { LoadedNonWorldPackages.Add(Package); } } // Reload non world package(s). if (LoadedNonWorldPackages.Num() > 0) { FText OutReloadErrorMsg; UPackageTools::ReloadPackages(LoadedNonWorldPackages, OutReloadErrorMsg, bInteractive ? EReloadPackagesInteractionMode::Interactive : EReloadPackagesInteractionMode::AssumePositive); if (!OutReloadErrorMsg.IsEmpty()) { UE_LOG(LogSourceControl, Warning, TEXT("%s"), *OutReloadErrorMsg.ToString()); } } // Reload world package(s). if (LoadedWorldPackages.Num() > 0) { FText OutReloadErrorMsg; UPackageTools::ReloadPackages(LoadedWorldPackages, OutReloadErrorMsg, bInteractive ? EReloadPackagesInteractionMode::Interactive : EReloadPackagesInteractionMode::AssumePositive); if (!OutReloadErrorMsg.IsEmpty()) { UE_LOG(LogSourceControl, Warning, TEXT("%s"), *OutReloadErrorMsg.ToString()); } } } // A world reload might have already deleted some objects, so check which missing ones are still valid. TArray ObjectsToDelete; if (ObjectsMissingOnDisk.Num() > 0) { for (TWeakObjectPtr Object : ObjectsMissingOnDisk) { if (UObject* ObjectToDelete = Object.Get()) { ObjectsToDelete.Add(ObjectToDelete); } } } // Delete and Unload assets... if (ObjectsToDelete.Num() > 0) { if (ObjectTools::DeleteObjectsUnchecked(ObjectsToDelete) != ObjectsToDelete.Num()) { UE_LOG(LogSourceControl, Warning, TEXT("Failed to unload some assets.")); } } // Re-cache the SCC state... SourceControlProvider.Execute(ISourceControlOperation::Create(), PackageFilenames, EConcurrency::Asynchronous); return bSuccess; } TArray USourceControlHelpers::GetSourceControlLocations(const bool bContentOnly) { TArray SourceControlLocations; TArray CustomProjects = ISourceControlModule::Get().GetCustomProjects(); if (CustomProjects.IsEmpty()) { TArray RootPaths; FPackageName::QueryRootContentPaths(RootPaths); for (const FString& RootPath : RootPaths) { const FString RootPathOnDisk = FPackageName::LongPackageNameToFilename(RootPath); SourceControlLocations.Add(FPaths::ConvertRelativePathToFull(RootPathOnDisk)); } if (!bContentOnly) { SourceControlLocations.Add(FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir())); SourceControlLocations.Add(FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath())); } } else { for (FSourceControlProjectInfo& ProjectInfo : CustomProjects) { for (FString& ContentDir : ProjectInfo.ContentDirectories) { SourceControlLocations.Add(FPaths::ConvertRelativePathToFull(MoveTemp(ContentDir))); } if (!bContentOnly) { SourceControlLocations.Add(FPaths::ConvertRelativePathToFull(MoveTemp(ProjectInfo.ProjectDirectory))); } } } return SourceControlLocations; } bool USourceControlHelpers::ListRevertablePackages(TArray& OutRevertablePackageNames) { if (!ISourceControlModule::Get().IsEnabled() || !ISourceControlModule::Get().GetProvider().IsAvailable()) { return false; } // update status for all packages TArray Filenames = GetSourceControlLocations(); ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); FSourceControlOperationRef Operation = ISourceControlOperation::Create(); if (SourceControlProvider.Execute(Operation, Filenames) != ECommandResult::Succeeded) { return false; } // Get a list of all the revertable packages TMap PackageStates; FEditorFileUtils::FindAllSubmittablePackageFiles(PackageStates, true); TArray PackageNames; PackageStates.GetKeys(PackageNames); TMap FileNamesToPackageNames; // Get a list of files pending delete TArray PendingDeleteItems = SourceControlProvider.GetCachedStateByPredicate( [&PackageNames, &FileNamesToPackageNames](const FSourceControlStateRef& State) { const FString& Filename = State->GetFilename(); if (FPackageName::IsPackageFilename(Filename)) { FString PackageName; FString FailureReason; if (!FPackageName::TryConvertFilenameToLongPackageName(Filename, PackageName, &FailureReason)) { UE_LOG(LogSourceControl, Warning, TEXT("%s"), *FailureReason); return false; } if (State->IsDeleted() && !PackageNames.Contains(PackageName)) { FileNamesToPackageNames.Add(Filename, PackageName); return true; } } return false; } ); // And append them to the list for (FSourceControlStateRef& Item : PendingDeleteItems) { const FString* PackageName = FileNamesToPackageNames.Find(Item->GetFilename()); if (PackageName) { PackageStates.Add(*PackageName, Item); } } for (auto& PackageState : PackageStates) { const FString PackageName = PackageState.Key; OutRevertablePackageNames.Add(PackageName); } return true; } bool USourceControlHelpers::RevertAllChangesAndReloadWorld() { TArray PackagesToReload; ListRevertablePackages(PackagesToReload); return RevertAndReloadPackages(PackagesToReload, /*bRevertAll=*/true, /*bReloadWorld=*/true); } bool USourceControlHelpers::RevertAndReloadPackages(const TArray& InPackagesToRevert, bool bRevertAll, bool bReloadWorld) { auto RevertOperation = [bRevertAll](const TArray& InPackagesToRevert) -> bool { auto OperationCompleteCallback = FSourceControlOperationComplete::CreateLambda([InPackagesToRevert](const FSourceControlOperationRef& Operation, ECommandResult::Type InResult) { if (Operation->GetName() == TEXT("Revert")) { TSharedRef RevertOperation = StaticCastSharedRef(Operation); ISourceControlModule::Get().GetOnFilesDeleted().Broadcast(RevertOperation->GetDeletedFiles()); // The revert may have caused added files to return to an uncontrolled state when not deleting newly added files // Handle that here, and return any newly uncontrolled files to the default uncontrolled changelist so they're not left in limbo if (GEditor && !RevertOperation->ShouldDeleteNewFiles()) { TArray SCStates; if (ISourceControlModule::Get().GetProvider().GetState(InPackagesToRevert, SCStates, EStateCacheUsage::ForceUpdate) == ECommandResult::Succeeded) { TArray NewlyUncontrolledFiles; for (const FSourceControlStateRef& SCState : SCStates) { if (!SCState->IsSourceControlled()) { NewlyUncontrolledFiles.Add(SCState->GetFilename()); } } if (NewlyUncontrolledFiles.Num() > 0) { FScopedDisableSourceControl DisableSourceControl; GEditor->AddDeferredMarkForAddFiles(NewlyUncontrolledFiles); GEditor->RunDeferredMarkForAddFiles(); } } } } }); auto RevertOperation = ISourceControlOperation::Create(); if (bRevertAll) { RevertOperation->SetRevertAll(true); } return ISourceControlModule::Get().GetProvider().Execute(RevertOperation, InPackagesToRevert, EConcurrency::Synchronous, OperationCompleteCallback) == ECommandResult::Succeeded; }; return ApplyOperationAndReloadPackages(InPackagesToRevert,RevertOperation, bReloadWorld); } #endif //!WITH_EDITOR bool USourceControlHelpers::RevertFiles(const TArray& InFiles, bool bSilent) { // If we have nothing to process, exit immediately if (InFiles.IsEmpty()) { return true; } // Determine file type and ensure they are in form source control wants // Even if some files were skipped, still apply to the others TArray SCFiles; bool bFilesSkipped = !SourceControlHelpersInternal::ConvertFilesToQualifiedPaths(InFiles, SCFiles, bSilent); const int32 NumFiles = SCFiles.Num(); // Ensure source control system is up and running ISourceControlProvider* Provider = SourceControlHelpersInternal::VerifySourceControl(bSilent); if (!Provider) { // Error or can't communicate with source control return false; } TArray SCStates; Provider->GetState(SCFiles, SCStates, EStateCacheUsage::ForceUpdate); TArray SCFilesToRevert; for (int32 Index = 0; Index < NumFiles; ++Index) { FString SCFile = SCFiles[Index]; FSourceControlStateRef SCState = SCStates[Index]; // Less error checking and info is made for multiple files than the single file version. // This multi-file version could be made similarly more sophisticated. if (SCState->CanRevert()) { SCFilesToRevert.Add(SCFile); } } bool bSuccess = !bFilesSkipped; if (SCFilesToRevert.Num()) { bSuccess &= Provider->Execute(ISourceControlOperation::Create(), SCFilesToRevert) == ECommandResult::Succeeded; } return bSuccess; } bool USourceControlHelpers::RevertUnchangedFile(const FString& InFile, bool bSilent) { // Determine file type and ensure it is in form source control wants FString SCFile = SourceControlHelpersInternal::ConvertFileToQualifiedPath(InFile, bSilent); if (SCFile.IsEmpty()) { return false; } // Ensure source control system is up and running ISourceControlProvider* Provider = SourceControlHelpersInternal::VerifySourceControl(bSilent); if (!Provider) { return false; } // Only revert file if they haven't had any changes made // Stuff single file in array for functions that require array TArray InFiles; InFiles.Add(SCFile); RevertUnchangedFiles(*Provider, InFiles); // Assume it succeeded return true; } bool USourceControlHelpers::RevertUnchangedFiles(const TArray& InFiles, bool bSilent) { // If we have nothing to process, exit immediately if (InFiles.IsEmpty()) { return true; } // Determine file types and ensure they are in form source control wants TArray FilePaths; SourceControlHelpersInternal::ConvertFilesToQualifiedPaths(InFiles, FilePaths, bSilent); ISourceControlProvider* Provider = SourceControlHelpersInternal::VerifySourceControl(bSilent); if (!Provider) { return false; } // Less error checking and info is made for multiple files than the single file version. // This multi-file version could be made similarly more sophisticated. // Only revert files if they haven't had any changes made RevertUnchangedFiles(*Provider, FilePaths); // Assume it succeeded return true; } bool USourceControlHelpers::CheckInFile(const FString& InFile, const FString& InDescription, bool bSilent, bool bKeepCheckedOut) { // Determine file type and ensure it is in form source control wants FString SCFile = SourceControlHelpersInternal::ConvertFileToQualifiedPath(InFile, bSilent); if (SCFile.IsEmpty()) { return false; } // Ensure source control system is up and running ISourceControlProvider* Provider = SourceControlHelpersInternal::VerifySourceControl(bSilent); if (!Provider) { return false; } TSharedRef CheckInOp = ISourceControlOperation::Create(); CheckInOp->SetDescription(FText::FromString(InDescription)); CheckInOp->SetKeepCheckedOut(bKeepCheckedOut); ECommandResult::Type Result = Provider->Execute(CheckInOp, SCFile); return Result == ECommandResult::Succeeded; } bool USourceControlHelpers::CheckInFiles(const TArray& InFiles, const FString& InDescription, bool bSilent, bool bKeepCheckedOut) { // If we have nothing to process, exit immediately if (InFiles.IsEmpty()) { return true; } ISourceControlProvider* Provider = SourceControlHelpersInternal::VerifySourceControl(bSilent); if (!Provider) { return false; } TArray FilePaths; // Even if some files were skipped, still apply to the others bool bFilesSkipped = !SourceControlHelpersInternal::ConvertFilesToQualifiedPaths(InFiles, FilePaths, bSilent); // Less error checking and info is made for multiple files than the single file version. // This multi-file version could be made similarly more sophisticated. TSharedRef CheckInOp = ISourceControlOperation::Create(); CheckInOp->SetDescription(FText::FromString(InDescription)); CheckInOp->SetKeepCheckedOut(bKeepCheckedOut); ECommandResult::Type Result = Provider->Execute(CheckInOp, FilePaths); return !bFilesSkipped && (Result == ECommandResult::Succeeded); } bool USourceControlHelpers::CopyFile(const FString& InSourcePath, const FString& InDestPath, bool bSilent) { // Determine file type and ensure it is in form source control wants FString SCSource = SourceControlHelpersInternal::ConvertFileToQualifiedPath(InSourcePath, bSilent); if (SCSource.IsEmpty()) { return false; } // Determine file type and ensure it is in form source control wants FString SCSourcExt(FPaths::GetExtension(SCSource, true)); FString SCDest(SourceControlHelpersInternal::ConvertFileToQualifiedPath(InDestPath, bSilent, /*bAllowDirectories*/false, *SCSourcExt)); if (SCDest.IsEmpty()) { return false; } // Ensure source control system is up and running ISourceControlProvider* Provider = SourceControlHelpersInternal::VerifySourceControl(bSilent); if (!Provider) { return false; } TSharedRef CopyOp = ISourceControlOperation::Create(); CopyOp->SetDestination(SCDest); ECommandResult::Type Result = Provider->Execute(CopyOp, SCSource); return Result == ECommandResult::Succeeded; } TArray USourceControlHelpers::QueryFileStates(const TArray InFiles, bool bSilent) { TArray States; SourceControlHelpersInternal::RunQueryFileStates(InFiles, EConcurrency::Synchronous, bSilent, [&States](FSourceControlState FileStateOut) { States.Emplace(MoveTemp(FileStateOut)); }); return States; } FSourceControlState USourceControlHelpers::QueryFileState(const FString& InFile, bool bSilent) { FSourceControlState State; State.Filename = InFile; SourceControlHelpersInternal::RunQueryFileStates(TArray {InFile}, EConcurrency::Synchronous, bSilent, [&State](FSourceControlState FileStateOut) { State = MoveTemp(FileStateOut); }); return State; } void USourceControlHelpers::AsyncQueryFileStates(FQueryFileStateDelegate FileStateCallback, TArray InFiles, bool bSilent) { SourceControlHelpersInternal::RunQueryFileStates(InFiles, EConcurrency::Asynchronous, bSilent, [FileStateCallback](FSourceControlState InState) { FileStateCallback.ExecuteIfBound(InState); }); } void USourceControlHelpers::AsyncQueryFileState(FQueryFileStateDelegate FileStateCallback, const FString& InFile, bool bSilent) { AsyncQueryFileStates(FileStateCallback, TArray {InFile}, bSilent); } bool USourceControlHelpers::GetFilesInDepotAtPath(const FString& Path, TArray& OutFilesList, bool bIncludeDeleted, bool bSilent, bool bIsFileRegexSearch) { TArray Paths; Paths.Add(Path); return GetFilesInDepotAtPaths(Paths, OutFilesList, bIncludeDeleted, bSilent, bIsFileRegexSearch); } bool USourceControlHelpers::GetFilesInDepotAtPaths(const TArray& Paths, TArray& OutFilesList, bool bIncludeDeleted, bool bSilent, bool bIsFileRegexSearch) { TRACE_CPUPROFILER_EVENT_SCOPE(USourceControlHelpers::GetFilesInDepotAtPaths); bool bSuccess = false; if (ISourceControlModule::Get().IsEnabled()) { ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); if (SourceControlProvider.IsAvailable()) { TArray PathsToQuery; for (const FString& Path : Paths) { FString RelativePath; FPackageName::TryConvertLongPackageNameToFilename(Path, RelativePath); FPaths::RemoveDuplicateSlashes(RelativePath); PathsToQuery.Add(RelativePath); } TSharedRef Operation = ISourceControlOperation::Create(); Operation->SetIncludeDeleted(bIncludeDeleted); Operation->SetQuiet(bSilent); Operation->SetSearchPattern(PathsToQuery); if (bIsFileRegexSearch) { Operation->SetMethodUsed(FGetFileList::EGetFileListMethod::FileRegexSearch); } ECommandResult::Type Result = SourceControlProvider.Execute(Operation, PathsToQuery, EConcurrency::Synchronous); bSuccess = (Result == ECommandResult::Succeeded); if (!bSuccess) { for (const FString& Path : Paths) { FFormatNamedArguments Arguments; Arguments.Add(TEXT("Path"), FText::FromString(Path)); SourceControlHelpersInternal::LogError(FText::Format(LOCTEXT("CouldNotGetFileList", "Could not get file list at path: {Path}."), Arguments), bSilent); } } else { OutFilesList = Operation->GetFilesList(); } } } return bSuccess; } static FString PackageFilename_Internal( const FString& InPackageName ) { FString Filename = InPackageName; // Get the filename by finding it on disk first if ( !FPackageName::IsMemoryPackage(InPackageName) && !FPackageName::DoesPackageExist(InPackageName, &Filename) ) { // The package does not exist on disk, see if we can find it in memory and predict the file extension // Only do this if the supplied package name is valid const bool bIncludeReadOnlyRoots = false; if ( FPackageName::IsValidLongPackageName(InPackageName, bIncludeReadOnlyRoots) ) { UPackage* Package = FindPackage(nullptr, *InPackageName); // This is a package in memory that has not yet been saved. Determine the extension and convert to a filename, if we do have the package, just assume normal asset extension const FString PackageExtension = Package && Package->ContainsMap() ? FPackageName::GetMapPackageExtension() : FPackageName::GetAssetPackageExtension(); Filename = FPackageName::LongPackageNameToFilename(InPackageName, PackageExtension); } } return Filename; } FString USourceControlHelpers::PackageFilename( const FString& InPackageName ) { return FPaths::ConvertRelativePathToFull(PackageFilename_Internal(InPackageName)); } FString USourceControlHelpers::PackageFilename( const UPackage* InPackage ) { FString Filename; if(InPackage != nullptr) { // Prefer using package loaded path to resolve file name as it properly resolves memory packages FString PackageLoadedPath = InPackage->GetLoadedPath().GetPackageName(); if (!InPackage->GetLoadedPath().IsEmpty() && FPackageName::IsMemoryPackage(PackageLoadedPath)) { const FString PackageExtension = InPackage->ContainsMap() ? FPackageName::GetMapPackageExtension() : FPackageName::GetAssetPackageExtension(); Filename = FPaths::ConvertRelativePathToFull(FPackageName::LongPackageNameToFilename(PackageLoadedPath, PackageExtension)); } else { Filename = FPaths::ConvertRelativePathToFull(PackageFilename_Internal(InPackage->GetName())); } } return Filename; } TArray USourceControlHelpers::PackageFilenames( const TArray& InPackages ) { TArray OutNames; OutNames.Reserve(InPackages.Num()); for (int32 PackageIndex = 0; PackageIndex < InPackages.Num(); PackageIndex++) { OutNames.Add(FPaths::ConvertRelativePathToFull(PackageFilename(InPackages[PackageIndex]))); } return OutNames; } TArray USourceControlHelpers::PackageFilenames( const TArray& InPackageNames ) { TArray OutNames; OutNames.Reserve(InPackageNames.Num()); for (int32 PackageIndex = 0; PackageIndex < InPackageNames.Num(); PackageIndex++) { OutNames.Add(FPaths::ConvertRelativePathToFull(PackageFilename_Internal(InPackageNames[PackageIndex]))); } return OutNames; } TArray USourceControlHelpers::AbsoluteFilenames( const TArray& InFileNames ) { TArray AbsoluteFiles; AbsoluteFiles.Reserve(InFileNames.Num()); for (const FString& FileName : InFileNames) { if(!FPaths::IsRelative(FileName)) { AbsoluteFiles.Add(FileName); } else { AbsoluteFiles.Add(FPaths::ConvertRelativePathToFull(FileName)); } FPaths::NormalizeFilename(AbsoluteFiles[AbsoluteFiles.Num() - 1]); } return AbsoluteFiles; } void USourceControlHelpers::RevertUnchangedFiles( ISourceControlProvider& InProvider, const TArray& InFiles ) { // Make sure we update the modified state of the files TSharedRef UpdateStatusOperation = ISourceControlOperation::Create(); UpdateStatusOperation->SetUpdateModifiedState(true); InProvider.Execute(UpdateStatusOperation, InFiles); TArray UnchangedFiles; TArray< TSharedRef > OutStates; InProvider.GetState(InFiles, OutStates, EStateCacheUsage::Use); for(TArray< TSharedRef >::TConstIterator It(OutStates); It; It++) { TSharedRef SourceControlState = *It; if(SourceControlState->IsCheckedOut() && !SourceControlState->IsModified()) { UnchangedFiles.Add(SourceControlState->GetFilename()); } } if(UnchangedFiles.Num()) { InProvider.Execute( ISourceControlOperation::Create(), UnchangedFiles ); } } bool USourceControlHelpers::AnnotateFile( ISourceControlProvider& InProvider, const FString& InLabel, const FString& InFile, TArray& OutLines ) { TArray< TSharedRef > Labels = InProvider.GetLabels( InLabel ); if(Labels.Num() > 0) { TSharedRef Label = Labels[0]; TArray< TSharedRef > Revisions; Label->GetFileRevisions(InFile, Revisions); if(Revisions.Num() > 0) { TSharedRef Revision = Revisions[0]; if(Revision->GetAnnotated(OutLines)) { return true; } } } return false; } bool USourceControlHelpers::AnnotateFile( ISourceControlProvider& InProvider, int32 InCheckInIdentifier, const FString& InFile, TArray& OutLines ) { TSharedRef UpdateStatusOperation = ISourceControlOperation::Create(); UpdateStatusOperation->SetUpdateHistory(true); if(InProvider.Execute(UpdateStatusOperation, InFile) == ECommandResult::Succeeded) { FSourceControlStatePtr State = InProvider.GetState(InFile, EStateCacheUsage::Use); if(State.IsValid()) { for(int32 HistoryIndex = State->GetHistorySize() - 1; HistoryIndex >= 0; HistoryIndex--) { // check that the changelist corresponds to this revision - we assume history is in latest-first order TSharedPtr Revision = State->GetHistoryItem(HistoryIndex); if(Revision.IsValid() && Revision->GetCheckInIdentifier() >= InCheckInIdentifier) { if(Revision->GetAnnotated(OutLines)) { return true; } } } } } return false; } bool USourceControlHelpers::CheckoutOrMarkForAdd( const FString& InDestFile, const FText& InFileDescription, const FOnPostCheckOut& OnPostCheckOut, FText& OutFailReason ) { bool bSucceeded = true; ISourceControlProvider& Provider = ISourceControlModule::Get().GetProvider(); // first check for source control check out if (ISourceControlModule::Get().IsEnabled()) { FSourceControlStatePtr SourceControlState = Provider.GetState(InDestFile, EStateCacheUsage::ForceUpdate); if (SourceControlState.IsValid()) { if (SourceControlState->IsSourceControlled() && SourceControlState->CanCheckout()) { ECommandResult::Type Result = Provider.Execute(ISourceControlOperation::Create(), InDestFile); bSucceeded = (Result == ECommandResult::Succeeded); if (!bSucceeded) { OutFailReason = FText::Format(LOCTEXT("SourceControlCheckoutError", "Could not check out {0} file."), InFileDescription); } } } } if (bSucceeded) { if(OnPostCheckOut.IsBound()) { bSucceeded = OnPostCheckOut.Execute(InDestFile, InFileDescription, OutFailReason); } } // mark for add now if needed if (bSucceeded && ISourceControlModule::Get().IsEnabled()) { FSourceControlStatePtr SourceControlState = Provider.GetState(InDestFile, EStateCacheUsage::Use); if (SourceControlState.IsValid()) { if (!SourceControlState->IsSourceControlled()) { ECommandResult::Type Result = Provider.Execute(ISourceControlOperation::Create(), InDestFile); bSucceeded = (Result == ECommandResult::Succeeded); if (!bSucceeded) { OutFailReason = FText::Format(LOCTEXT("SourceControlMarkForAddError", "Could not mark {0} file for add."), InFileDescription); } } } } return bSucceeded; } bool USourceControlHelpers::CopyFileUnderSourceControl( const FString& InDestFile, const FString& InSourceFile, const FText& InFileDescription, FText& OutFailReason) { struct Local { static bool CopyFile(const FString& InDestinationFile, const FText& InFileDesc, FText& OutFailureReason, FString InFileToCopy) { const bool bReplace = true; const bool bEvenIfReadOnly = true; bool bSucceeded = (IFileManager::Get().Copy(*InDestinationFile, *InFileToCopy, bReplace, bEvenIfReadOnly) == COPY_OK); if (!bSucceeded) { OutFailureReason = FText::Format(LOCTEXT("ExternalImageCopyError", "Could not overwrite {0} file."), InFileDesc); } return bSucceeded; } }; return CheckoutOrMarkForAdd(InDestFile, InFileDescription, FOnPostCheckOut::CreateStatic(&Local::CopyFile, InSourceFile), OutFailReason); } namespace { bool CopyPackage_Internal(UPackage* DestPackage, UPackage* SourcePackage, FCopy::ECopyMethod CopyMethod, EStateCacheUsage::Type StateCacheUsage) { if (ISourceControlModule::Get().IsEnabled()) { ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); const FString SourceFilename = USourceControlHelpers::PackageFilename(SourcePackage); FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(SourceFilename, StateCacheUsage); if (SourceControlState.IsValid() && SourceControlState->IsSourceControlled()) { FString DestinationPath = USourceControlHelpers::PackageFilename(DestPackage); FSourceControlStatePtr DestinationState = SourceControlProvider.GetState(DestinationPath, StateCacheUsage); TSharedRef CopyOperation = ISourceControlOperation::Create(); CopyOperation->SetDestination(DestinationPath); CopyOperation->CopyMethod = CopyMethod; CopyOperation->ResolveMethod = (DestinationState.IsValid() && !DestinationState->CanRevert() && !IFileManager::Get().FileExists(*DestinationPath)) ? FCopy::EResolveMethod::AcceptSource : FCopy::EResolveMethod::AcceptTarget; return (SourceControlProvider.Execute(CopyOperation, SourceFilename) == ECommandResult::Succeeded); } } return false; } } bool USourceControlHelpers::BranchPackage(UPackage* DestPackage, UPackage* SourcePackage, EStateCacheUsage::Type StateCacheUsage) { return CopyPackage_Internal(DestPackage, SourcePackage, FCopy::ECopyMethod::Branch, StateCacheUsage); } bool USourceControlHelpers::CopyPackage(UPackage* DestPackage, UPackage* SourcePackage, EStateCacheUsage::Type StateCacheUsage) { return CopyPackage_Internal(DestPackage, SourcePackage, FCopy::ECopyMethod::Add, StateCacheUsage); } const FString& USourceControlHelpers::GetSettingsIni() { if (ISourceControlModule::Get().GetUseGlobalSettings()) { return GetGlobalSettingsIni(); } else { static FString SourceControlSettingsIni; if (SourceControlSettingsIni.Len() == 0) { FConfigContext Context = FConfigContext::ReadIntoGConfig(); Context.Load(TEXT("SourceControlSettings"), SourceControlSettingsIni); } return SourceControlSettingsIni; } } const FString& USourceControlHelpers::GetGlobalSettingsIni() { static FString SourceControlGlobalSettingsIni; if (SourceControlGlobalSettingsIni.Len() == 0) { FConfigContext Context = FConfigContext::ReadIntoGConfig(); Context.GeneratedConfigDir = FPaths::EngineSavedDir() + TEXT("Config/"); Context.ProjectConfigDir = (""); // don't load anything from project configs Context.Load(TEXT("SourceControlSettings"), SourceControlGlobalSettingsIni); } return SourceControlGlobalSettingsIni; } bool USourceControlHelpers::GetAssetData(const FString& InFileName, TArray& OutAssets, TArray* OutDependencies) { FString PackageName; if (FPackageName::TryConvertFilenameToLongPackageName(InFileName, PackageName)) { return GetAssetData(InFileName, PackageName, OutAssets, OutDependencies); } else { return false; } } bool USourceControlHelpers::GetAssetDataFromPackage(const FString& PackageName, TArray& OutAssets, TArray* OutDependencies) { return GetAssetData(PackageFilename(PackageName), PackageName, OutAssets, OutDependencies); } bool USourceControlHelpers::GetAssetData(const FString & InFileName, const FString& InPackageName, TArray& OutAssets, TArray* OutDependencies) { const bool bGetDependencies = (OutDependencies != nullptr); OutAssets.Reset(); if (bGetDependencies) { OutDependencies->Reset(); } // Try the registry first FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); AssetRegistryModule.Get().GetAssetsByPackageName(*InPackageName, OutAssets, true); if (OutAssets.Num() > 0) { // Assets are already in the cache, we can query dependencies directly if (bGetDependencies) { AssetRegistryModule.Get().GetDependencies(*InPackageName, *OutDependencies, UE::AssetRegistry::EDependencyCategory::Package, UE::AssetRegistry::EDependencyQuery::Hard); } return true; } // Filter on improbable file extensions EPackageExtension PackageExtension = FPackagePath::ParseExtension(InFileName); if (PackageExtension == EPackageExtension::Unspecified || PackageExtension == EPackageExtension::Custom) { return false; } // If nothing was done, try to get the data explicitly IAssetRegistry::FLoadPackageRegistryData LoadedData(bGetDependencies); AssetRegistryModule.Get().LoadPackageRegistryData(InFileName, LoadedData); OutAssets = MoveTemp(LoadedData.Data); if (bGetDependencies) { *OutDependencies = MoveTemp(LoadedData.DataDependencies); } return OutAssets.Num() > 0; } bool USourceControlHelpers::GetAssetDataFromFileHistory(const FString& InFileName, TArray& OutAssets, TArray* OutDependencies, int64 MaxFetchSize) { OutAssets.Reset(); if (OutDependencies) { OutDependencies->Reset(); } ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); // Get the SCC state FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(InFileName, EStateCacheUsage::Use); if (SourceControlState.IsValid()) { return GetAssetDataFromFileHistory(SourceControlState, OutAssets, OutDependencies, MaxFetchSize); } else { return false; } } bool USourceControlHelpers::GetAssetDataFromFileHistory(FSourceControlStatePtr InSourceControlState, TArray& OutAssets, TArray* OutDependencies /* = nullptr */, int64 MaxFetchSize /* = -1 */) { check(InSourceControlState.IsValid()); OutAssets.Reset(); if (OutDependencies) { OutDependencies->Reset(); } // This code is similar to what's done in UAssetToolsImpl::DiffAgainstDepot but we'll force it quiet to prevent recursion issues if (InSourceControlState->GetHistorySize() == 0) { ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); TSharedRef UpdateStatusOperation = ISourceControlOperation::Create(); UpdateStatusOperation->SetUpdateHistory(true); UpdateStatusOperation->SetQuiet(true); SourceControlProvider.Execute(UpdateStatusOperation, InSourceControlState->GetFilename()); } if (InSourceControlState->GetHistorySize() > 0) { TSharedPtr Revision = InSourceControlState->GetHistoryItem(0); check(Revision.IsValid()); const bool bShouldGetFile = (MaxFetchSize < 0 || MaxFetchSize >(int64)Revision->GetFileSize()); FString TempFileName; if (bShouldGetFile && Revision->Get(TempFileName)) { return GetAssetData(TempFileName, OutAssets, OutDependencies); } } return false; } FScopedSourceControl::FScopedSourceControl() { bInitSourceControl = !ISourceControlModule::Get().GetProvider().IsAvailable(); if (bInitSourceControl) { ISourceControlModule::Get().GetProvider().Init(); } } FScopedSourceControl::~FScopedSourceControl() { if (bInitSourceControl) { ISourceControlModule::Get().GetProvider().Close(); } } ISourceControlProvider& FScopedSourceControl::GetProvider() { return ISourceControlModule::Get().GetProvider(); } #undef LOCTEXT_NAMESPACE