// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) // // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt // or copy at http://opensource.org/licenses/MIT) #include "GitSourceControlMenu.h" #include "GitSourceControlModule.h" #include "GitSourceControlProvider.h" #include "GitSourceControlOperations.h" #include "GitSourceControlUtils.h" #include "ISourceControlModule.h" #include "ISourceControlOperation.h" #include "SourceControlOperations.h" #include "LevelEditor.h" #include "Widgets/Notifications/SNotificationList.h" #include "Framework/Notifications/NotificationManager.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Misc/MessageDialog.h" #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 #include "Styling/AppStyle.h" #else #include "EditorStyleSet.h" #endif #include "PackageTools.h" #include "FileHelpers.h" #include "Logging/MessageLog.h" #include "SourceControlHelpers.h" #include "SourceControlWindows.h" #if ENGINE_MAJOR_VERSION == 5 #include "ToolMenus.h" #include "ToolMenuContext.h" #include "ToolMenuMisc.h" #endif #include "UObject/Linker.h" static const FName GitSourceControlMenuTabName(TEXT("GitSourceControlMenu")); #define LOCTEXT_NAMESPACE "GitSourceControl" TWeakPtr FGitSourceControlMenu::OperationInProgressNotification; void FGitSourceControlMenu::Register() { #if ENGINE_MAJOR_VERSION >= 5 FToolMenuOwnerScoped SourceControlMenuOwner("GitSourceControlMenu"); if (UToolMenus* ToolMenus = UToolMenus::Get()) { UToolMenu* SourceControlMenu = ToolMenus->ExtendMenu("StatusBar.ToolBar.SourceControl"); FToolMenuSection& Section = SourceControlMenu->AddSection("GitSourceControlActions", LOCTEXT("GitSourceControlMenuHeadingActions", "Git"), FToolMenuInsert(NAME_None, EToolMenuInsertType::First)); AddMenuExtension(Section); } #else // Register the extension with the level editor FLevelEditorModule* LevelEditorModule = FModuleManager::GetModulePtr(TEXT("LevelEditor")); if (LevelEditorModule) { FLevelEditorModule::FLevelEditorMenuExtender ViewMenuExtender = FLevelEditorModule::FLevelEditorMenuExtender::CreateRaw(this, &FGitSourceControlMenu::OnExtendLevelEditorViewMenu); auto& MenuExtenders = LevelEditorModule->GetAllLevelEditorToolbarSourceControlMenuExtenders(); MenuExtenders.Add(ViewMenuExtender); ViewMenuExtenderHandle = MenuExtenders.Last().GetHandle(); } #endif } void FGitSourceControlMenu::Unregister() { #if ENGINE_MAJOR_VERSION >= 5 if (UToolMenus* ToolMenus = UToolMenus::Get()) { UToolMenus::Get()->UnregisterOwnerByName("GitSourceControlMenu"); } #else // Unregister the level editor extensions FLevelEditorModule* LevelEditorModule = FModuleManager::GetModulePtr("LevelEditor"); if (LevelEditorModule) { LevelEditorModule->GetAllLevelEditorToolbarSourceControlMenuExtenders().RemoveAll([=](const FLevelEditorModule::FLevelEditorMenuExtender& Extender) { return Extender.GetHandle() == ViewMenuExtenderHandle; }); } #endif } bool FGitSourceControlMenu::HaveRemoteUrl() const { const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); return !GitSourceControl.GetProvider().GetRemoteUrl().IsEmpty(); } /// Prompt to save or discard all packages bool FGitSourceControlMenu::SaveDirtyPackages() { const bool bPromptUserToSave = true; const bool bSaveMapPackages = true; const bool bSaveContentPackages = true; const bool bFastSave = false; const bool bNotifyNoPackagesSaved = false; const bool bCanBeDeclined = true; // If the user clicks "don't save" this will continue and lose their changes bool bHadPackagesToSave = false; bool bSaved = FEditorFileUtils::SaveDirtyPackages(bPromptUserToSave, bSaveMapPackages, bSaveContentPackages, bFastSave, bNotifyNoPackagesSaved, bCanBeDeclined, &bHadPackagesToSave); // bSaved can be true if the user selects to not save an asset by unchecking it and clicking "save" if (bSaved) { TArray DirtyPackages; FEditorFileUtils::GetDirtyWorldPackages(DirtyPackages); FEditorFileUtils::GetDirtyContentPackages(DirtyPackages); bSaved = DirtyPackages.Num() == 0; } return bSaved; } // Ask the user if they want to stash any modification and try to unstash them afterward, which could lead to conflicts bool FGitSourceControlMenu::StashAwayAnyModifications() { bool bStashOk = true; FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); const FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); const FString& PathToRespositoryRoot = Provider.GetPathToRepositoryRoot(); const FString& PathToGitBinary = Provider.GetGitBinaryPath(); const TArray ParametersStatus{"--porcelain --untracked-files=no"}; TArray InfoMessages; TArray ErrorMessages; // Check if there is any modification to the working tree const bool bStatusOk = GitSourceControlUtils::RunCommand(TEXT("status"), PathToGitBinary, PathToRespositoryRoot, ParametersStatus, FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); if ((bStatusOk) && (InfoMessages.Num() > 0)) { // Ask the user before stashing const FText DialogText(LOCTEXT("SourceControlMenu_Stash_Ask", "Stash (save) all modifications of the working tree? Required to Sync/Pull!")); const EAppReturnType::Type Choice = FMessageDialog::Open(EAppMsgType::OkCancel, DialogText); if (Choice == EAppReturnType::Ok) { const TArray ParametersStash{ "save \"Stashed by Unreal Engine Git Plugin\"" }; bStashMadeBeforeSync = GitSourceControlUtils::RunCommand(TEXT("stash"), PathToGitBinary, PathToRespositoryRoot, ParametersStash, FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); if (!bStashMadeBeforeSync) { FMessageLog SourceControlLog("SourceControl"); SourceControlLog.Warning(LOCTEXT("SourceControlMenu_StashFailed", "Stashing away modifications failed!")); SourceControlLog.Notify(); } } else { bStashOk = false; } } return bStashOk; } // Unstash any modifications if a stash was made at the beginning of the Sync operation void FGitSourceControlMenu::ReApplyStashedModifications() { if (bStashMadeBeforeSync) { FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); const FString& PathToRespositoryRoot = Provider.GetPathToRepositoryRoot(); const FString& PathToGitBinary = Provider.GetGitBinaryPath(); const TArray ParametersStash{ "pop" }; TArray InfoMessages; TArray ErrorMessages; const bool bUnstashOk = GitSourceControlUtils::RunCommand(TEXT("stash"), PathToGitBinary, PathToRespositoryRoot, ParametersStash, FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); if (!bUnstashOk) { FMessageLog SourceControlLog("SourceControl"); SourceControlLog.Warning(LOCTEXT("SourceControlMenu_UnstashFailed", "Unstashing previously saved modifications failed!")); SourceControlLog.Notify(); } } } void FGitSourceControlMenu::SyncClicked() { if (!OperationInProgressNotification.IsValid()) { // Ask the user to save any dirty assets opened in Editor const bool bSaved = SaveDirtyPackages(); if (bSaved) { FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); // Launch a "Sync" operation TSharedRef SyncOperation = ISourceControlOperation::Create(); #if ENGINE_MAJOR_VERSION >= 5 const ECommandResult::Type Result = Provider.Execute(SyncOperation, FSourceControlChangelistPtr(), FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete)); #else const ECommandResult::Type Result = Provider.Execute(SyncOperation, FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete)); #endif if (Result == ECommandResult::Succeeded) { // Display an ongoing notification during the whole operation (packages will be reloaded at the completion of the operation) DisplayInProgressNotification(SyncOperation->GetInProgressString()); } else { // Report failure with a notification and Reload all packages DisplayFailureNotification(SyncOperation->GetName()); } } else { FMessageLog SourceControlLog("SourceControl"); SourceControlLog.Warning(LOCTEXT("SourceControlMenu_Sync_Unsaved", "Save All Assets before attempting to Sync!")); SourceControlLog.Notify(); } } else { FMessageLog SourceControlLog("SourceControl"); SourceControlLog.Warning(LOCTEXT("SourceControlMenu_InProgress", "Revision control operation already in progress")); SourceControlLog.Notify(); } } void FGitSourceControlMenu::CommitClicked() { if (OperationInProgressNotification.IsValid()) { FMessageLog SourceControlLog("SourceControl"); SourceControlLog.Warning(LOCTEXT("SourceControlMenu_InProgress", "Revision control operation already in progress")); SourceControlLog.Notify(); return; } FLevelEditorModule & LevelEditorModule = FModuleManager::Get().LoadModuleChecked("LevelEditor"); FSourceControlWindows::ChoosePackagesToCheckIn(nullptr); } void FGitSourceControlMenu::PushClicked() { if (!OperationInProgressNotification.IsValid()) { // Launch a "Push" Operation FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); TSharedRef PushOperation = ISourceControlOperation::Create(); #if ENGINE_MAJOR_VERSION >= 5 const ECommandResult::Type Result = Provider.Execute(PushOperation, FSourceControlChangelistPtr(), FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete)); #else const ECommandResult::Type Result = Provider.Execute(PushOperation, FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete)); #endif if (Result == ECommandResult::Succeeded) { // Display an ongoing notification during the whole operation DisplayInProgressNotification(PushOperation->GetInProgressString()); } else { // Report failure with a notification DisplayFailureNotification(PushOperation->GetName()); } } else { FMessageLog SourceControlLog("SourceControl"); SourceControlLog.Warning(LOCTEXT("SourceControlMenu_InProgress", "Revision control operation already in progress")); SourceControlLog.Notify(); } } void FGitSourceControlMenu::RevertClicked() { if (OperationInProgressNotification.IsValid()) { FMessageLog SourceControlLog("SourceControl"); SourceControlLog.Warning(LOCTEXT("SourceControlMenu_InProgress", "Revision control operation already in progress")); SourceControlLog.Notify(); return; } // Ask the user before reverting all! const FText DialogText(LOCTEXT("SourceControlMenu_Revert_Ask", "Revert all modifications of the working tree?")); const EAppReturnType::Type Choice = FMessageDialog::Open(EAppMsgType::OkCancel, DialogText); if (Choice != EAppReturnType::Ok) { return; } // make sure we update the SCC status of all packages (this could take a long time, so we will run it as a background task) const TArray Filenames { FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()), FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir()), FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath()) }; ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); FSourceControlOperationRef Operation = ISourceControlOperation::Create(); #if ENGINE_MAJOR_VERSION >= 5 SourceControlProvider.Execute(Operation, FSourceControlChangelistPtr(), Filenames, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateStatic(&FGitSourceControlMenu::RevertAllCallback)); #else SourceControlProvider.Execute(Operation, Filenames, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateStatic(&FGitSourceControlMenu::RevertAllCallback)); #endif FNotificationInfo Info(LOCTEXT("SourceControlMenuRevertAll", "Checking for assets to revert...")); Info.bFireAndForget = false; Info.ExpireDuration = 0.0f; Info.FadeOutDuration = 1.0f; if (SourceControlProvider.CanCancelOperation(Operation)) { Info.ButtonDetails.Add(FNotificationButtonInfo( LOCTEXT("SourceControlMenuRevertAll_CancelButton", "Cancel"), LOCTEXT("SourceControlMenuRevertAll_CancelButtonTooltip", "Cancel the revert operation."), FSimpleDelegate::CreateStatic(&FGitSourceControlMenu::RevertAllCancelled, Operation) )); } OperationInProgressNotification = FSlateNotificationManager::Get().AddNotification(Info); if (OperationInProgressNotification.IsValid()) { OperationInProgressNotification.Pin()->SetCompletionState(SNotificationItem::CS_Pending); } } void FGitSourceControlMenu::RevertAllCallback(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult) { if (InResult != ECommandResult::Succeeded) { return; } // Get a list of all the checked out packages TArray PackageNames; TArray LoadedPackages; TMap PackageStates; FEditorFileUtils::FindAllSubmittablePackageFiles(PackageStates, true); for (TMap::TConstIterator PackageIter(PackageStates); PackageIter; ++PackageIter) { const FString PackageName = *PackageIter.Key(); const FSourceControlStatePtr CurPackageSCCState = PackageIter.Value(); UPackage* Package = FindPackage(nullptr, *PackageName); if (Package != nullptr) { LoadedPackages.Add(Package); if (!Package->IsFullyLoaded()) { FlushAsyncLoading(); Package->FullyLoad(); } ResetLoaders(Package); } PackageNames.Add(PackageName); } const auto FileNames = SourceControlHelpers::PackageFilenames(PackageNames); // Launch a "Revert" Operation FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); const TSharedRef RevertOperation = ISourceControlOperation::Create(); #if ENGINE_MAJOR_VERSION >= 5 const auto Result = Provider.Execute(RevertOperation, FSourceControlChangelistPtr(), FileNames); #else const auto Result = Provider.Execute(RevertOperation, FileNames); #endif RemoveInProgressNotification(); if (Result != ECommandResult::Succeeded) { DisplayFailureNotification(TEXT("Revert")); } else { DisplaySucessNotification(TEXT("Revert")); } GitSourceControlUtils::ReloadPackages(LoadedPackages); #if ENGINE_MAJOR_VERSION >= 5 Provider.Execute(ISourceControlOperation::Create(), FSourceControlChangelistPtr(), FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous); #else Provider.Execute(ISourceControlOperation::Create(), FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous); #endif } void FGitSourceControlMenu::RefreshClicked() { if (!OperationInProgressNotification.IsValid()) { FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); // Launch an "GitFetch" Operation TSharedRef RefreshOperation = ISourceControlOperation::Create(); RefreshOperation->bUpdateStatus = true; #if ENGINE_MAJOR_VERSION >= 5 const ECommandResult::Type Result = Provider.Execute(RefreshOperation, FSourceControlChangelistPtr(), FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete)); #else const ECommandResult::Type Result = Provider.Execute(RefreshOperation, FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete)); #endif if (Result == ECommandResult::Succeeded) { // Display an ongoing notification during the whole operation DisplayInProgressNotification(RefreshOperation->GetInProgressString()); } else { // Report failure with a notification DisplayFailureNotification(RefreshOperation->GetName()); } } else { FMessageLog SourceControlLog("SourceControl"); SourceControlLog.Warning(LOCTEXT("SourceControlMenu_InProgress", "Revision control operation already in progress")); SourceControlLog.Notify(); } } // Display an ongoing notification during the whole operation void FGitSourceControlMenu::DisplayInProgressNotification(const FText& InOperationInProgressString) { if (!OperationInProgressNotification.IsValid()) { FNotificationInfo Info(InOperationInProgressString); Info.bFireAndForget = false; Info.ExpireDuration = 0.0f; Info.FadeOutDuration = 1.0f; OperationInProgressNotification = FSlateNotificationManager::Get().AddNotification(Info); if (OperationInProgressNotification.IsValid()) { OperationInProgressNotification.Pin()->SetCompletionState(SNotificationItem::CS_Pending); } } } void FGitSourceControlMenu::RevertAllCancelled(FSourceControlOperationRef InOperation) { ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); SourceControlProvider.CancelOperation(InOperation); if (OperationInProgressNotification.IsValid()) { OperationInProgressNotification.Pin()->ExpireAndFadeout(); } OperationInProgressNotification.Reset(); } // Remove the ongoing notification at the end of the operation void FGitSourceControlMenu::RemoveInProgressNotification() { if (OperationInProgressNotification.IsValid()) { OperationInProgressNotification.Pin()->ExpireAndFadeout(); OperationInProgressNotification.Reset(); } } // Display a temporary success notification at the end of the operation void FGitSourceControlMenu::DisplaySucessNotification(const FName& InOperationName) { const FText NotificationText = FText::Format( LOCTEXT("SourceControlMenu_Success", "{0} operation was successful!"), FText::FromName(InOperationName) ); FNotificationInfo Info(NotificationText); Info.bUseSuccessFailIcons = true; #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 Info.Image = FAppStyle::GetBrush(TEXT("NotificationList.SuccessImage")); #else Info.Image = FEditorStyle::GetBrush(TEXT("NotificationList.SuccessImage")); #endif FSlateNotificationManager::Get().AddNotification(Info); #if UE_BUILD_DEBUG UE_LOG(LogSourceControl, Log, TEXT("%s"), *NotificationText.ToString()); #endif } // Display a temporary failure notification at the end of the operation void FGitSourceControlMenu::DisplayFailureNotification(const FName& InOperationName) { const FText NotificationText = FText::Format( LOCTEXT("SourceControlMenu_Failure", "Error: {0} operation failed!"), FText::FromName(InOperationName) ); FNotificationInfo Info(NotificationText); Info.ExpireDuration = 8.0f; FSlateNotificationManager::Get().AddNotification(Info); UE_LOG(LogSourceControl, Error, TEXT("%s"), *NotificationText.ToString()); } void FGitSourceControlMenu::OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult) { RemoveInProgressNotification(); if ((InOperation->GetName() == "Sync") || (InOperation->GetName() == "Revert")) { // Unstash any modifications if a stash was made at the beginning of the Sync operation ReApplyStashedModifications(); // Reload packages that where unlinked at the beginning of the Sync/Revert operation GitSourceControlUtils::ReloadPackages(PackagesToReload); } // Report result with a notification if (InResult == ECommandResult::Succeeded) { DisplaySucessNotification(InOperation->GetName()); } else { DisplayFailureNotification(InOperation->GetName()); } } #if ENGINE_MAJOR_VERSION >= 5 void FGitSourceControlMenu::AddMenuExtension(FToolMenuSection& Builder) #else void FGitSourceControlMenu::AddMenuExtension(FMenuBuilder& Builder) #endif { Builder.AddMenuEntry( #if ENGINE_MAJOR_VERSION >= 5 "GitPush", #endif LOCTEXT("GitPush", "Push pending local commits"), LOCTEXT("GitPushTooltip", "Push all pending local commits to the remote server."), #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 FSlateIcon(FAppStyle::GetAppStyleSetName(), "SourceControl.Actions.Submit"), #else FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Submit"), #endif FUIAction( FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::PushClicked), FCanExecuteAction::CreateRaw(this, &FGitSourceControlMenu::HaveRemoteUrl) ) ); Builder.AddMenuEntry( #if ENGINE_MAJOR_VERSION >= 5 "GitSync", #endif LOCTEXT("GitSync", "Pull"), LOCTEXT("GitSyncTooltip", "Update all files in the local repository to the latest version of the remote server."), #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 FSlateIcon(FAppStyle::GetAppStyleSetName(), "SourceControl.Actions.Sync"), #else FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Sync"), #endif FUIAction( FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::SyncClicked), FCanExecuteAction::CreateRaw(this, &FGitSourceControlMenu::HaveRemoteUrl) ) ); Builder.AddMenuEntry( #if ENGINE_MAJOR_VERSION >= 5 "GitRevert", #endif LOCTEXT("GitRevert", "Revert"), LOCTEXT("GitRevertTooltip", "Revert all files in the repository to their unchanged state."), #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 FSlateIcon(FAppStyle::GetAppStyleSetName(), "SourceControl.Actions.Revert"), #else FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Revert"), #endif FUIAction( FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::RevertClicked), FCanExecuteAction() ) ); Builder.AddMenuEntry( #if ENGINE_MAJOR_VERSION >= 5 "GitRefresh", #endif LOCTEXT("GitRefresh", "Refresh"), LOCTEXT("GitRefreshTooltip", "Update the revision control status of all files in the local repository."), #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 FSlateIcon(FAppStyle::GetAppStyleSetName(), "SourceControl.Actions.Refresh"), #else FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Refresh"), #endif FUIAction( FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::RefreshClicked), FCanExecuteAction() ) ); } #if ENGINE_MAJOR_VERSION < 5 TSharedRef FGitSourceControlMenu::OnExtendLevelEditorViewMenu(const TSharedRef CommandList) { TSharedRef Extender(new FExtender()); Extender->AddMenuExtension( "SourceControlActions", EExtensionHook::After, nullptr, FMenuExtensionDelegate::CreateRaw(this, &FGitSourceControlMenu::AddMenuExtension)); return Extender; } #endif #undef LOCTEXT_NAMESPACE