// Copyright Epic Games, Inc. All Rights Reserved. #include "UncontrolledChangelistsModule.h" #include "Algo/AnyOf.h" #include "Algo/Copy.h" #include "Algo/Find.h" #include "Algo/ForEach.h" #include "Algo/Transform.h" #include "AssetRegistry/AssetRegistryModule.h" #include "Dom/JsonObject.h" #include "FileHelpers.h" #include "HAL/FileManager.h" #include "HAL/IConsoleManager.h" #include "ISourceControlModule.h" #include "ISourceControlProvider.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "PackagesDialog.h" #include "Serialization/JsonSerializer.h" #include "Serialization/JsonWriter.h" #include "SourceControlOperations.h" #include "Styling/SlateTypes.h" #include "UObject/ObjectSaveContext.h" #define LOCTEXT_NAMESPACE "UncontrolledChangelists" static TAutoConsoleVariable CVarUncontrolledChangelistsEnable( TEXT("UncontrolledChangelists.Enable"), false, TEXT("Enables Uncontrolled Changelists (experimental).") ); void FUncontrolledChangelistsModule::StartupModule() { // Adds Default Uncontrolled Changelist if it is not already present. FUncontrolledChangelist DefaultUncontrolledChangelist(FUncontrolledChangelist::DEFAULT_UNCONTROLLED_CHANGELIST_GUID, FUncontrolledChangelist::DEFAULT_UNCONTROLLED_CHANGELIST_NAME.ToString()); UncontrolledChangelistsStateCache.FindOrAdd(MoveTemp(DefaultUncontrolledChangelist), MakeShareable(new FUncontrolledChangelistState(DefaultUncontrolledChangelist))); LoadState(); FAssetRegistryModule& AssetRegistryModule = FModuleManager::GetModuleChecked(TEXT("AssetRegistry")); IAssetRegistry& AssetRegistry = AssetRegistryModule.Get(); OnAssetAddedDelegateHandle = AssetRegistry.OnAssetAdded().AddLambda([](const struct FAssetData& AssetData) { Get().OnAssetAdded(AssetData); }); OnObjectPreSavedDelegateHandle = FCoreUObjectDelegates::OnObjectPreSave.AddLambda([](UObject* InAsset, const FObjectPreSaveContext& InPreSaveContext) { Get().OnObjectPreSaved(InAsset, InPreSaveContext); }); TArray AssetData; AssetRegistry.GetAllAssets(AssetData); Algo::ForEach(AssetData, [this](const struct FAssetData& AssetData) { OnAssetAdded(AssetData); }); } void FUncontrolledChangelistsModule::ShutdownModule() { FAssetRegistryModule* AssetRegistryModulePtr = static_cast(FModuleManager::Get().GetModule(TEXT("AssetRegistry"))); // Check in case AssetRegistry has already been shutdown. if (AssetRegistryModulePtr != nullptr) { AssetRegistryModulePtr->Get().OnAssetAdded().Remove(OnAssetAddedDelegateHandle); } FCoreUObjectDelegates::OnObjectPreSave.Remove(OnObjectPreSavedDelegateHandle); } bool FUncontrolledChangelistsModule::IsEnabled() const { return CVarUncontrolledChangelistsEnable.GetValueOnGameThread(); } TArray FUncontrolledChangelistsModule::GetChangelistStates() const { TArray UncontrolledChangelistStates; if (IsEnabled()) { Algo::Transform(UncontrolledChangelistsStateCache, UncontrolledChangelistStates, [](const auto& Pair) { return Pair.Value; }); } return UncontrolledChangelistStates; } bool FUncontrolledChangelistsModule::OnMakeWritable(const FString& InFilename) { bool bHasStateChanged = false; if (!IsEnabled()) { return false; } FUncontrolledChangelist DefaultUncontrolledChangelist(FUncontrolledChangelist::DEFAULT_UNCONTROLLED_CHANGELIST_GUID, FUncontrolledChangelist::DEFAULT_UNCONTROLLED_CHANGELIST_NAME.ToString()); FUncontrolledChangelistsStateCache::ValueType& UncontrolledChangelistState = UncontrolledChangelistsStateCache.FindOrAdd(MoveTemp(DefaultUncontrolledChangelist), MakeShareable(new FUncontrolledChangelistState(DefaultUncontrolledChangelist))); bHasStateChanged = UncontrolledChangelistState->AddFiles({ InFilename }, FUncontrolledChangelistState::ECheckFlags::NotCheckedOut); if (bHasStateChanged) { OnStateChanged(); } return bHasStateChanged; } void FUncontrolledChangelistsModule::UpdateStatus() { bool bHasStateChanged = false; if (!IsEnabled()) { return; } for (FUncontrolledChangelistsStateCache::ElementType& Pair : UncontrolledChangelistsStateCache) { FUncontrolledChangelistsStateCache::ValueType& UncontrolledChangelistState = Pair.Value; bHasStateChanged |= UncontrolledChangelistState->UpdateStatus(); } if (bHasStateChanged) { OnStateChanged(); } } FText FUncontrolledChangelistsModule::GetReconcileStatus() const { return FText::Format(LOCTEXT("ReconcileStatus", "Assets to check for reconcile: {0}"), FText::AsNumber(AddedAssetsCache.Num())); } bool FUncontrolledChangelistsModule::OnReconcileAssets() { if ((!IsEnabled()) || AddedAssetsCache.IsEmpty()) { return false; } CleanAssetsCaches(); bool bHasStateChanged = AddFilesToDefaultUncontrolledChangelist(AddedAssetsCache.Array(), FUncontrolledChangelistState::ECheckFlags::All); AddedAssetsCache.Empty(); return bHasStateChanged; } void FUncontrolledChangelistsModule::OnAssetAdded(const struct FAssetData& AssetData) { if (!IsEnabled()) { return; } FPackagePath PackagePath; if (!FPackagePath::TryFromPackageName(AssetData.PackageName, PackagePath)) { return; } if (!FPackageName::DoesPackageExist(PackagePath, &PackagePath)) { return; // If the package does not exist on disk there is nothing more to do } const FString LocalFullPath(PackagePath.GetLocalFullPath()); if (LocalFullPath.IsEmpty()) { return; } FString Fullpath = FPaths::ConvertRelativePathToFull(LocalFullPath); if (Fullpath.IsEmpty()) { return; } if (!IFileManager::Get().IsReadOnly(*Fullpath)) { AddedAssetsCache.Add(MoveTemp(Fullpath)); } } void FUncontrolledChangelistsModule::OnObjectPreSaved(UObject* InAsset, const FObjectPreSaveContext& InPreSaveContext) { if (!IsEnabled()) { return; } FString Fullpath = FPaths::ConvertRelativePathToFull(InPreSaveContext.GetTargetFilename()); if (Fullpath.IsEmpty()) { return; } AddedAssetsCache.Add(MoveTemp(Fullpath)); } void FUncontrolledChangelistsModule::MoveFilesToUncontrolledChangelist(const TArray& InControlledFileStates, const TArray& InUncontrolledFileStates, const FUncontrolledChangelist& InUncontrolledChangelist) { bool bHasStateChanged = false; if (!IsEnabled()) { return; } FUncontrolledChangelistsStateCache::ValueType* ChangelistState = UncontrolledChangelistsStateCache.Find(InUncontrolledChangelist); if (ChangelistState == nullptr) { return; } TArray Filenames; Algo::Transform(InControlledFileStates, Filenames, [](const FSourceControlStateRef& State) { return State->GetFilename(); }); ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); auto RevertOperation = ISourceControlOperation::Create(); // Revert controlled files RevertOperation->SetSoftRevert(true); SourceControlProvider.Execute(RevertOperation, Filenames); // Removes selected Uncontrolled Files from their Uncontrolled Changelists for (const auto& Pair : UncontrolledChangelistsStateCache) { const FUncontrolledChangelistStateRef& UncontrolledChangelistState = Pair.Value; UncontrolledChangelistState->RemoveFiles(InUncontrolledFileStates); } Algo::Transform(InUncontrolledFileStates, Filenames, [](const FSourceControlStateRef& State) { return State->GetFilename(); }); // Add all files to their UncontrolledChangelist bHasStateChanged = (*ChangelistState)->AddFiles(Filenames, FUncontrolledChangelistState::ECheckFlags::None); if (bHasStateChanged) { OnStateChanged(); } } void FUncontrolledChangelistsModule::MoveFilesToControlledChangelist(const TArray& InUncontrolledFileStates, const FSourceControlChangelistPtr& InChangelist) { if (!IsEnabled()) { return; } TArray UncontrolledFilenames; Algo::Transform(InUncontrolledFileStates, UncontrolledFilenames, [](const FSourceControlStateRef& State) { return State->GetFilename(); }); MoveFilesToControlledChangelist(UncontrolledFilenames, InChangelist); } void FUncontrolledChangelistsModule::MoveFilesToControlledChangelist(const TArray& InUncontrolledFiles, const FSourceControlChangelistPtr& InChangelist) { if (!IsEnabled()) { return; } ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); TArray UpdatedFilestates; // Get updated filestates to check Checkout capabilities. SourceControlProvider.GetState(InUncontrolledFiles, UpdatedFilestates, EStateCacheUsage::ForceUpdate); TArray PackageConflicts; TArray FilesToAdd; TArray FilesToCheckout; TArray FilesToDelete; // Check if we can Checkout files or mark for add for (const FSourceControlStateRef& Filestate : UpdatedFilestates) { if (!Filestate->IsSourceControlled()) { FilesToAdd.Add(Filestate->GetFilename()); } else if (!IFileManager::Get().FileExists(*Filestate->GetFilename())) { FilesToDelete.Add(Filestate->GetFilename()); } else if (Filestate->CanCheckout()) { FilesToCheckout.Add(Filestate->GetFilename()); } } bool bCanProceed = true; // If we detected conflict, asking user if we should proceed. if (!PackageConflicts.IsEmpty()) { bCanProceed = ShowConflictDialog(PackageConflicts); } if (bCanProceed) { if (!FilesToCheckout.IsEmpty()) { SourceControlProvider.Execute(ISourceControlOperation::Create(), InChangelist, FilesToCheckout); } if (!FilesToAdd.IsEmpty()) { SourceControlProvider.Execute(ISourceControlOperation::Create(), InChangelist, FilesToAdd); } if (!FilesToDelete.IsEmpty()) { SourceControlProvider.Execute(ISourceControlOperation::Create(), InChangelist, FilesToDelete); } // UpdateStatus so UncontrolledChangelists can remove files from their cache if they were present before checkout. UpdateStatus(); } } bool FUncontrolledChangelistsModule::ShowConflictDialog(TArray InPackageConflicts) { FPackagesDialogModule& CheckoutPackagesDialogModule = FModuleManager::LoadModuleChecked(TEXT("PackagesDialog")); CheckoutPackagesDialogModule.CreatePackagesDialog(LOCTEXT("CheckoutPackagesDialogTitle", "Check Out Assets"), LOCTEXT("CheckoutPackagesDialogMessage", "Conflict detected in the following assets:"), true); CheckoutPackagesDialogModule.AddButton(DRT_CheckOut, LOCTEXT("Dlg_CheckOutButton", "Check Out"), LOCTEXT("Dlg_CheckOutTooltip", "Attempt to Check Out Assets")); CheckoutPackagesDialogModule.AddButton(DRT_Cancel, LOCTEXT("Dlg_Cancel", "Cancel"), LOCTEXT("Dlg_CancelTooltip", "Cancel Request")); CheckoutPackagesDialogModule.SetWarning(LOCTEXT("CheckoutPackagesWarnMessage", "Warning: These assets are locked or not at the head revision. You may lose your changes if you continue, as you will be unable to submit them to source control.")); for (UPackage* Conflict : InPackageConflicts) { CheckoutPackagesDialogModule.AddPackageItem(Conflict, ECheckBoxState::Undetermined); } EDialogReturnType UserResponse = CheckoutPackagesDialogModule.ShowPackagesDialog(); return UserResponse == DRT_CheckOut; } void FUncontrolledChangelistsModule::OnStateChanged() { OnUncontrolledChangelistModuleChanged.Broadcast(); SaveState(); } void FUncontrolledChangelistsModule::CleanAssetsCaches() { // Remove files we are already tracking in Uncontrolled Changelists for (FUncontrolledChangelistsStateCache::ElementType& Pair : UncontrolledChangelistsStateCache) { FUncontrolledChangelistsStateCache::ValueType& UncontrolledChangelistState = Pair.Value; UncontrolledChangelistState->RemoveDuplicates(AddedAssetsCache); } } bool FUncontrolledChangelistsModule::AddFilesToDefaultUncontrolledChangelist(const TArray& InFilenames, const FUncontrolledChangelistState::ECheckFlags InCheckFlags) { bool bHasStateChanged = false; FUncontrolledChangelist DefaultUncontrolledChangelist(FUncontrolledChangelist::DEFAULT_UNCONTROLLED_CHANGELIST_GUID, FUncontrolledChangelist::DEFAULT_UNCONTROLLED_CHANGELIST_NAME.ToString()); FUncontrolledChangelistsStateCache::ValueType& UncontrolledChangelistState = UncontrolledChangelistsStateCache.FindOrAdd(MoveTemp(DefaultUncontrolledChangelist), MakeShareable(new FUncontrolledChangelistState(DefaultUncontrolledChangelist))); // Try to add files, they will be added only if they pass the required checks bHasStateChanged = UncontrolledChangelistState->AddFiles(InFilenames, InCheckFlags); if (bHasStateChanged) { OnStateChanged(); } return bHasStateChanged; } void FUncontrolledChangelistsModule::SaveState() const { TSharedPtr RootObject = MakeShareable(new FJsonObject); TArray> UncontrolledChangelistsArray; RootObject->SetNumberField(VERSION_NAME, VERSION_NUMBER); for (const auto& Pair : UncontrolledChangelistsStateCache) { const FUncontrolledChangelist& UncontrolledChangelist = Pair.Key; FUncontrolledChangelistStateRef UncontrolledChangelistState = Pair.Value; TSharedPtr UncontrolledChangelistObject = MakeShareable(new FJsonObject); UncontrolledChangelist.Serialize(UncontrolledChangelistObject.ToSharedRef()); UncontrolledChangelistState->Serialize(UncontrolledChangelistObject.ToSharedRef()); UncontrolledChangelistsArray.Add(MakeShareable(new FJsonValueObject(UncontrolledChangelistObject))); } RootObject->SetArrayField(CHANGELISTS_NAME, UncontrolledChangelistsArray); using FStringWriter = TJsonWriter>; using FStringWriterFactory = TJsonWriterFactory>; FString RootObjectStr; TSharedRef Writer = FStringWriterFactory::Create(&RootObjectStr); FJsonSerializer::Serialize(RootObject.ToSharedRef(), Writer); FFileHelper::SaveStringToFile(RootObjectStr, *GetPersistentFilePath()); } void FUncontrolledChangelistsModule::LoadState() { FString ImportJsonString; TSharedPtr RootObject; uint32 VersionNumber = 0; const TArray>* UncontrolledChangelistsArray = nullptr; if (!FFileHelper::LoadFileToString(ImportJsonString, *GetPersistentFilePath())) { return; } TSharedRef> JsonReader = TJsonReaderFactory<>::Create(ImportJsonString); if (!FJsonSerializer::Deserialize(JsonReader, RootObject)) { UE_LOG(LogSourceControl, Error, TEXT("Cannot deserialize RootObject.")); return; } if (!RootObject->TryGetNumberField(VERSION_NAME, VersionNumber)) { UE_LOG(LogSourceControl, Error, TEXT("Cannot get field %s."), VERSION_NAME); return; } if (VersionNumber != VERSION_NUMBER) { UE_LOG(LogSourceControl, Error, TEXT("Version number is invalid (file: %u, current: %u)."), VersionNumber, VERSION_NUMBER); return; } if (!RootObject->TryGetArrayField(CHANGELISTS_NAME, UncontrolledChangelistsArray)) { UE_LOG(LogSourceControl, Error, TEXT("Cannot get field %s."), CHANGELISTS_NAME); return; } for (const TSharedPtr& JsonValue : *UncontrolledChangelistsArray) { FUncontrolledChangelist TempKey; TSharedRef JsonObject = JsonValue->AsObject().ToSharedRef(); if (!TempKey.Deserialize(JsonObject)) { UE_LOG(LogSourceControl, Error, TEXT("Cannot deserialize FUncontrolledChangelist.")); continue; } FUncontrolledChangelistsStateCache::ValueType& UncontrolledChangelistState = UncontrolledChangelistsStateCache.FindOrAdd(MoveTemp(TempKey), MakeShareable(new FUncontrolledChangelistState(TempKey))); UncontrolledChangelistState->Deserialize(JsonObject); } UE_LOG(LogSourceControl, Display, TEXT("Uncontrolled Changelist persistency file loaded %s"), *GetPersistentFilePath()); } FString FUncontrolledChangelistsModule::GetPersistentFilePath() const { return FPaths::ProjectSavedDir() + TEXT("SourceControl/UncontrolledChangelists.json"); } FString FUncontrolledChangelistsModule::GetUObjectPackageFullpath(const UObject* InObject) const { FString Fullpath = TEXT(""); if (InObject == nullptr) { return Fullpath; } const UPackage* Package = InObject->GetPackage(); if (Package == nullptr) { return Fullpath; } const FString LocalFullPath(Package->GetLoadedPath().GetLocalFullPath()); if (LocalFullPath.IsEmpty()) { return Fullpath; } Fullpath = FPaths::ConvertRelativePathToFull(LocalFullPath); return Fullpath; } IMPLEMENT_MODULE(FUncontrolledChangelistsModule, UncontrolledChangelists); #undef LOCTEXT_NAMESPACE