// Copyright Epic Games, Inc. All Rights Reserved. #include "SourceControlWindows.h" #include "SSourceControlSubmit.h" #include "SSourceControlCheckedOutDialog.h" #include "AssetViewUtils.h" #include "FileHelpers.h" #include "ISourceControlModule.h" #include "ISourceControlWindowsModule.h" #include "SourceControlHelpers.h" #include "SourceControlOperations.h" #include "Framework/Application/SlateApplication.h" #include "Framework/Notifications/NotificationManager.h" #include "Logging/MessageLog.h" #include "Logging/TokenizedMessage.h" #include "Misc/MessageDialog.h" #include "Widgets/Notifications/SNotificationList.h" #include "SourceControlSettings.h" #include "Bookmarks/BookmarkScoped.h" #include "HAL/IConsoleManager.h" #if SOURCE_CONTROL_WITH_SLATE #define LOCTEXT_NAMESPACE "SourceControlWindows" static FAutoConsoleCommand CCmdRunChoosePackagesToCheckIn = FAutoConsoleCommand( TEXT("RevisionControl.ChoosePackagesToCheckIn"), TEXT("Display all content changes in the current project and allow the user to select which changes to submit to revision control"), FConsoleCommandDelegate::CreateLambda([]() { FSourceControlWindows::ChoosePackagesToCheckIn(); })); //--------------------------------------------------------------------------------------- // FCheckinResultInfo FCheckinResultInfo::FCheckinResultInfo() : Result(ECommandResult::Failed) , bAutoCheckedOut(false) { } //--------------------------------------------------------------------------------------- // Helper function(s) static bool SaveDirtyPackages(bool bUseDialog) { const bool bPromptUserToSave = bUseDialog; 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 bSaved = FEditorFileUtils::SaveDirtyPackages(bPromptUserToSave, bSaveMapPackages, bSaveContentPackages, bFastSave, bNotifyNoPackagesSaved, bCanBeDeclined); // 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; } //--------------------------------------------------------------------------------------- // FSourceControlWindows TWeakPtr FSourceControlWindows::ChoosePackagesToCheckInNotification; bool FSourceControlWindows::ChoosePackagesToCheckIn(const FSourceControlWindowsOnCheckInComplete& OnCompleteDelegate) { if (!ISourceControlModule::Get().IsEnabled()) { FCheckinResultInfo ResultInfo; ResultInfo.Description = LOCTEXT("SourceControlDisabled", "Revision control is not enabled."); OnCompleteDelegate.ExecuteIfBound(ResultInfo); return false; } if (!ISourceControlModule::Get().GetProvider().IsAvailable()) { FCheckinResultInfo ResultInfo; ResultInfo.Description = LOCTEXT("NoSCCConnection", "No connection to revision control available!"); FMessageLog EditorErrors("EditorErrors"); EditorErrors.Warning(ResultInfo.Description)->AddToken( FDocumentationToken::Create(TEXT("Engine/UI/SourceControl"))); EditorErrors.Notify(); OnCompleteDelegate.ExecuteIfBound(ResultInfo); return false; } if (ISourceControlModule::Get().GetProvider().UsesSnapshots()) { SaveDirtyPackages(/*bUseDialog=*/false); } // Start selection process... // 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) TArray Filenames = SourceControlHelpers::GetSourceControlLocations(); // make sure the SourceControlProvider state cache is populated as well ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); FSourceControlOperationRef Operation = ISourceControlOperation::Create(); SourceControlProvider.Execute( Operation, Filenames, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateStatic(&FSourceControlWindows::ChoosePackagesToCheckInCallback, OnCompleteDelegate)); if (ChoosePackagesToCheckInNotification.IsValid()) { ChoosePackagesToCheckInNotification.Pin()->ExpireAndFadeout(); } FNotificationInfo Info(LOCTEXT("ChooseAssetsToCheckInIndicator", "Checking for assets to check in...")); Info.bFireAndForget = false; Info.ExpireDuration = 0.0f; Info.FadeOutDuration = 1.0f; if (SourceControlProvider.CanCancelOperation(Operation)) { Info.ButtonDetails.Add(FNotificationButtonInfo( LOCTEXT("ChoosePackagesToCheckIn_CancelButton", "Cancel"), LOCTEXT("ChoosePackagesToCheckIn_CancelButtonTooltip", "Cancel the check in operation."), FSimpleDelegate::CreateStatic(&FSourceControlWindows::ChoosePackagesToCheckInCancelled, Operation) )); } ChoosePackagesToCheckInNotification = FSlateNotificationManager::Get().AddNotification(Info); if (ChoosePackagesToCheckInNotification.IsValid()) { ChoosePackagesToCheckInNotification.Pin()->SetCompletionState(SNotificationItem::CS_Pending); } return true; } bool FSourceControlWindows::CanChoosePackagesToCheckIn() { ISourceControlModule& SourceControlModule = ISourceControlModule::Get(); if (ISourceControlModule::Get().IsEnabled() && ISourceControlModule::Get().GetProvider().IsAvailable() && !ChoosePackagesToCheckInNotification.IsValid()) { if (SourceControlModule.GetProvider().GetNumLocalChanges().IsSet()) { return SourceControlModule.GetProvider().GetNumLocalChanges().GetValue() > 0; } else { return true; } } return false; } bool FSourceControlWindows::SyncLatest() { return SyncRevision(TEXT("")); } bool FSourceControlWindows::SyncRevision(const FString& InRevision) { bool bSaved = SaveDirtyPackages(/*bUseDialog=*/true); // if not properly saved, ask for confirmation from the user before continuing. if (!bSaved) { FText DialogText = NSLOCTEXT("SourceControlCommands", "UnsavedWarningText", "Warning: There are modified assets which are not being saved. If you sync to latest you may lose your unsaved changes. Do you want to continue?"); FText DialogTitle = NSLOCTEXT("SourceControlCommands", "UnsavedWarningTitle", "Unsaved changes"); EAppReturnType::Type DialogResult = FMessageDialog::Open(EAppMsgType::YesNo, DialogText, DialogTitle); bSaved = (DialogResult == EAppReturnType::Yes); } // if properly saved or confirmation given, find all packages and use source control to update them. if (bSaved) { bool bSuccess = AssetViewUtils::SyncRevisionFromSourceControl(InRevision); if (!bSuccess) { FText Message(LOCTEXT("SCC_Sync_Failed", "Failed to sync files!")); FMessageLog("SourceControl").Notify(Message); } return bSuccess; } return false; } bool FSourceControlWindows::CanSyncLatest() { ISourceControlModule& SourceControlModule = ISourceControlModule::Get(); if (SourceControlModule.IsEnabled() && SourceControlModule.GetProvider().IsAvailable()) { if (SourceControlModule.GetProvider().IsAtLatestRevision().IsSet()) { return !SourceControlModule.GetProvider().IsAtLatestRevision().GetValue(); } else { return true; } } return false; } bool FSourceControlWindows::PromptForCheckin(FCheckinResultInfo& OutResultInfo, const TArray& InPackageNames, const TArray& InPendingDeletePaths, const TArray& InConfigFiles, bool bUseSourceControlStateCache) { ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Get filenames for packages and config to be checked in TArray AllFiles = SourceControlHelpers::PackageFilenames(InPackageNames); AllFiles.Append(InConfigFiles); // Prepare a list of files to have their states updated if (!bUseSourceControlStateCache) { TArray UpdateRequest; UpdateRequest.Append(AllFiles); // If there are pending delete paths to update, add them here. UpdateRequest.Append(InPendingDeletePaths); // Force an update on everything that's been requested if (UpdateRequest.Num() > 0) { SourceControlProvider.Execute(ISourceControlOperation::Create(), UpdateRequest); } } // Get file status of packages and config TArray States; SourceControlProvider.GetState(AllFiles, States, EStateCacheUsage::Use); if (InPendingDeletePaths.Num() > 0) { // Get any files pending delete TArray PendingDeleteItems = SourceControlProvider.GetCachedStateByPredicate( [&States](const FSourceControlStateRef& State) { return State->IsDeleted() // if the states already contains the pending delete do not bother appending it && !States.Contains(State); } ); // And append them to the list States.Append(PendingDeleteItems); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Exit if no assets needing check in if (States.Num() == 0) { OutResultInfo.Result = ECommandResult::Succeeded; OutResultInfo.Description = LOCTEXT("NoAssetsToCheckIn", "No assets to check in!"); FMessageLog EditorErrors("EditorErrors"); EditorErrors.Warning(OutResultInfo.Description); EditorErrors.Notify(); // Consider it a success even if no files were checked in return true; } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Create a submit files window TSharedRef NewWindow = SNew(SWindow) .Title(NSLOCTEXT("SourceControl.SubmitWindow", "Title", "Submit Files")) .SizingRule(ESizingRule::UserSized) .ClientSize(FVector2D(600, 400)) .SupportsMaximize(true) .SupportsMinimize(false); TSharedRef SourceControlWidget = SNew(SSourceControlSubmitWidget) .ParentWindow(NewWindow) .Items(States) .AllowUncheckFiles_Lambda([&]() { if (SourceControlProvider.IsAvailable()) { return !SourceControlProvider.UsesSnapshots(); } return true; }) .AllowDiffAgainstDepot(SourceControlProvider.AllowsDiffAgainstDepot()); NewWindow->SetContent( SourceControlWidget ); FSlateApplication::Get().AddModalWindow(NewWindow, NULL); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Exit if cancelled by user if (SourceControlWidget->GetResult() == ESubmitResults::SUBMIT_CANCELED) { OutResultInfo.Result = ECommandResult::Cancelled; OutResultInfo.Description = LOCTEXT("CheckinCancelled", "File check in cancelled."); return false; } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Sync to latest if using snapshots. if (ISourceControlModule::Get().GetProvider().UsesSnapshots()) { const bool bSyncNeeded = FSourceControlWindows::CanSyncLatest(); if (bSyncNeeded) { FBookmarkScoped BookmarkScoped; // Preserve viewport camera orientation. if (!FSourceControlWindows::SyncLatest()) { OutResultInfo.Description = LOCTEXT("SCC_Checkin_Aborted_Sync", "File check in aborted because the sync to the latest snapshot failed."); return false; } TArray Conflicts = ISourceControlModule::Get().GetProvider().GetCachedStateByPredicate( [](const FSourceControlStateRef& State) { return State->IsConflicted(); } ); const bool bConflictsRemaining = (Conflicts.Num() > 0); if (bConflictsRemaining) { OutResultInfo.Description = LOCTEXT("SCC_Checkin_Aborted_Conflicts", "File check in aborted because the sync to the latest snapshot resulted in conflicts that need to be resolved."); return false; } } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Get description from the dialog FChangeListDescription Description; SourceControlWidget->FillChangeListDescription(Description); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Revert any unchanged files if (Description.FilesForSubmit.Num() > 0) { SourceControlHelpers::RevertUnchangedFiles(SourceControlProvider, Description.FilesForSubmit); if (ISourceControlModule::Get().GetCustomProjects().IsEmpty()) { // Make sure all files are still checked out for (int32 VerifyIndex = Description.FilesForSubmit.Num() - 1; VerifyIndex >= 0; --VerifyIndex) { FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(Description.FilesForSubmit[VerifyIndex], EStateCacheUsage::Use); if (SourceControlState.IsValid() && !SourceControlState->IsCheckedOut() && !SourceControlState->IsAdded() && !SourceControlState->IsDeleted()) { Description.FilesForSubmit.RemoveAt(VerifyIndex); } } } else { // For project-based source control, we want to go through with a check in attempt even when // files are not checked out by the current user, and generate a warning dialog } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Mark files for add as needed bool bSuccess = true; // Overall success bool bAddSuccess = true; bool bCheckinSuccess = true; bool bCheckinCancelled = false; TArray CombinedFileList = Description.FilesForAdd; CombinedFileList.Append(Description.FilesForSubmit); if (Description.FilesForAdd.Num() > 0) { bAddSuccess = SourceControlProvider.Execute(ISourceControlOperation::Create(), Description.FilesForAdd) == ECommandResult::Succeeded; bSuccess &= bAddSuccess; OutResultInfo.FilesAdded = Description.FilesForAdd; if (!bAddSuccess) { // Note that this message may be overwritten with a checkin error below. OutResultInfo.Description = LOCTEXT("SCC_Add_Files_Error", "One or more files were not able to be marked for add to version control!"); } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Any files to check in? if (CombinedFileList.Num() == 0) { OutResultInfo.Result = bSuccess ? ECommandResult::Succeeded : ECommandResult::Failed; if (OutResultInfo.Description.IsEmpty()) { OutResultInfo.Description = LOCTEXT("SCC_No_Files", "No files were selected to check in to version control."); } return bSuccess; } // first check if there is a submit override bound if (ISourceControlWindowsModule::Get().SubmitOverrideDelegate.IsBound()) { SSubmitOverrideParameters SubmitOverrideParameters; SubmitOverrideParameters.Description = Description.Description.ToString(); SubmitOverrideParameters.ToSubmit.SetSubtype>(CombinedFileList); FSubmitOverrideReply SubmitOverrideReply = ISourceControlWindowsModule::Get().SubmitOverrideDelegate.Execute(SubmitOverrideParameters); switch (SubmitOverrideReply) { ////////////////////////////////////////////////////////// case FSubmitOverrideReply::Handled: { OutResultInfo.Result = ECommandResult::Succeeded; OutResultInfo.Description = LOCTEXT("SCC_Checkin_SubmitOverride_Succeeded", "Successfully invoked the submit override!"); return true; } ////////////////////////////////////////////////////////// case FSubmitOverrideReply::Error: { OutResultInfo.Result = ECommandResult::Failed; OutResultInfo.Description = LOCTEXT("SCC_Checkin_SubmitOverride_Failed", "Failed to invoke the submit override!"); return false; } ////////////////////////////////////////////////////////// case FSubmitOverrideReply::ProviderNotSupported: default: // continue default flow break; } } FText VirtualizationFailureMsg; if (!TryToVirtualizeFilesToSubmit(CombinedFileList, Description.Description, VirtualizationFailureMsg)) { FMessageLog("SourceControl").Notify(VirtualizationFailureMsg); OutResultInfo.Result = ECommandResult::Failed; OutResultInfo.Description = VirtualizationFailureMsg; return false; } // Check in files TSharedRef CheckInOperation = ISourceControlOperation::Create(); CheckInOperation->SetDescription(Description.Description); CheckInOperation->SetKeepCheckedOut(SourceControlWidget->WantToKeepCheckedOut()); ECommandResult::Type CheckInResult = SourceControlProvider.Execute(CheckInOperation, CombinedFileList); bCheckinSuccess = CheckInResult == ECommandResult::Succeeded; bCheckinCancelled = CheckInResult == ECommandResult::Cancelled; bSuccess &= bCheckinSuccess; if (bCheckinSuccess) { // report success with a notification FNotificationInfo Info(CheckInOperation->GetSuccessMessage()); Info.ExpireDuration = 8.0f; Info.HyperlinkText = LOCTEXT("SCC_Checkin_ShowLog", "Show Message Log"); Info.Hyperlink = FSimpleDelegate::CreateStatic([](){ FMessageLog("SourceControl").Open(EMessageSeverity::Info, true); }); FSlateNotificationManager::Get().AddNotification(Info); // also add to the log FMessageLog("SourceControl").Info(CheckInOperation->GetSuccessMessage()); OutResultInfo.Result = ECommandResult::Succeeded; OutResultInfo.Description = CheckInOperation->GetSuccessMessage(); OutResultInfo.FilesSubmitted = Description.FilesForSubmit; } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Abort if cancelled if (bCheckinCancelled) { FText Message(LOCTEXT("CheckinCancelled", "File check in cancelled.")); OutResultInfo.Result = ECommandResult::Cancelled; OutResultInfo.Description = Message; return false; } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Exit if errors if (!bSuccess) { FText Message(LOCTEXT("SCC_Checkin_Failed", "Failed to check in files!")); FMessageLog("SourceControl").Notify(Message); OutResultInfo.Result = ECommandResult::Failed; if (!bCheckinSuccess || OutResultInfo.Description.IsEmpty()) { OutResultInfo.Description = Message; } return false; } SourceControlWidget->ClearChangeListDescription(); return true; } bool FSourceControlWindows::PromptForCheckin(bool bUseSourceControlStateCache, const TArray& InPackageNames, const TArray& InPendingDeletePaths, const TArray& InConfigFiles) { FCheckinResultInfo ResultInfo; return PromptForCheckin(ResultInfo, InPackageNames, InPendingDeletePaths, InConfigFiles, bUseSourceControlStateCache); } bool FSourceControlWindows::PromptForCheckedOut(bool bUseSourceControlStateCache, TArray& InFileNames, FCheckedOutSetupInfo& InSetupInfo) { TArray Items; if (InFileNames.Num() > 0) { ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); if (!bUseSourceControlStateCache) { SourceControlProvider.Execute(ISourceControlOperation::Create(), InFileNames); } SourceControlProvider.GetState(InFileNames, Items, EStateCacheUsage::Use); } TSharedRef Window = SNew(SWindow) .SizingRule(ESizingRule::Autosized) .ClientSize(FVector2D(800, 600)) .SupportsMaximize(false) .SupportsMinimize(false); TSharedRef Widget = SNew(SSourceControlCheckedOutDialog) .ParentWindow(Window) .Items(Items) .ShowColumnAssetName(InSetupInfo.bShowColumnAssetName) .ShowColumnAssetClass(InSetupInfo.bShowColumnAssetClass) .ShowColumnUserName(InSetupInfo.bShowColumnUserName) .MessageText(InSetupInfo.MessageText) .CloseText(InSetupInfo.CloseText) .CheckBoxText(InSetupInfo.CheckboxText); Window->SetTitle(InSetupInfo.TitleText); Window->SetContent( Widget ); FSlateApplication::Get().AddModalWindow(Window, NULL); return Widget->IsCheckBoxChecked(); } // Note that: // - FSourceControlWindows::DisplayRevisionHistory() is defined in SSourceControlHistory.cpp // - FSourceControlWindows::PromptForRevert() is defined in SSourceControlRevert.cpp void FSourceControlWindows::ChoosePackagesToCheckInCompleted(const TArray& LoadedPackages, const TArray& PackageNames, const TArray& ConfigFiles, FCheckinResultInfo& OutResultInfo) { if (ChoosePackagesToCheckInNotification.IsValid()) { ChoosePackagesToCheckInNotification.Pin()->ExpireAndFadeout(); } ChoosePackagesToCheckInNotification.Reset(); // Prompt the user to ask if they would like to first save any dirty packages they are trying to check-in const FEditorFileUtils::EPromptReturnCode UserResponse = FEditorFileUtils::PromptForCheckoutAndSave(LoadedPackages, true, true); // If the user elected to save dirty packages, but one or more of the packages failed to save properly OR if the user // canceled out of the prompt, don't follow through on the check-in process const bool bShouldProceed = (UserResponse == FEditorFileUtils::EPromptReturnCode::PR_Success || UserResponse == FEditorFileUtils::EPromptReturnCode::PR_Declined); if (!bShouldProceed) { // If a failure occurred, alert the user that the check-in was aborted. This warning shouldn't be necessary if the user cancelled // from the dialog, because they obviously intended to cancel the whole operation. if (UserResponse == FEditorFileUtils::EPromptReturnCode::PR_Failure) { OutResultInfo.Description = NSLOCTEXT("UnrealEd", "SCC_Checkin_Aborted", "Check-in aborted as a result of save failure."); FMessageDialog::Open(EAppMsgType::Ok, OutResultInfo.Description); } return; } bool bUseSourceControlStateCache = true; TArray PendingDeletePaths = SourceControlHelpers::GetSourceControlLocations(); PromptForCheckin(OutResultInfo, PackageNames, PendingDeletePaths, ConfigFiles, bUseSourceControlStateCache); } void FSourceControlWindows::ChoosePackagesToCheckInCancelled(FSourceControlOperationRef InOperation) { ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); SourceControlProvider.CancelOperation(InOperation); if (ChoosePackagesToCheckInNotification.IsValid()) { ChoosePackagesToCheckInNotification.Pin()->ExpireAndFadeout(); } ChoosePackagesToCheckInNotification.Reset(); } void FSourceControlWindows::ChoosePackagesToCheckInCallback(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult, FSourceControlWindowsOnCheckInComplete OnCompleteDelegate) { if (ChoosePackagesToCheckInNotification.IsValid()) { ChoosePackagesToCheckInNotification.Pin()->ExpireAndFadeout(); } ChoosePackagesToCheckInNotification.Reset(); FCheckinResultInfo ResultInfo; if (InResult != ECommandResult::Succeeded) { switch (InResult) { case ECommandResult::Cancelled: ResultInfo.Result = ECommandResult::Cancelled; ResultInfo.Description = LOCTEXT("CheckInCancelled", "Check in cancelled."); break; case ECommandResult::Failed: { ResultInfo.Description = LOCTEXT("CheckInOperationFailed", "Failed checking revision control status!"); FMessageLog EditorErrors("EditorErrors"); EditorErrors.Warning(ResultInfo.Description); EditorErrors.Notify(); } } OnCompleteDelegate.ExecuteIfBound(ResultInfo); return; } // Get a list of all the checked out packages TArray PackageNames; TArray LoadedPackages; TMap PackageStates; FEditorFileUtils::FindAllSubmittablePackageFiles(PackageStates, true); TArray ConfigFilesToSubmit; for (TMap::TConstIterator PackageIter(PackageStates); PackageIter; ++PackageIter) { const FString PackageName = *PackageIter.Key(); UPackage* Package = FindPackage(nullptr, *PackageName); if (Package != nullptr) { LoadedPackages.Add(Package); } PackageNames.Add(PackageName); } // Get a list of all the checked out project files TMap ProjectFileStates; FEditorFileUtils::FindAllSubmittableProjectFiles(ProjectFileStates); for (TMap::TConstIterator It(ProjectFileStates); It; ++It) { ConfigFilesToSubmit.Add(It.Key()); } // Get a list of all the checked out config files TMap ConfigFileStates; FEditorFileUtils::FindAllSubmittableConfigFiles(ConfigFileStates); for (TMap::TConstIterator It(ConfigFileStates); It; ++It) { ConfigFilesToSubmit.Add(It.Key()); } ChoosePackagesToCheckInCompleted(LoadedPackages, PackageNames, ConfigFilesToSubmit, ResultInfo); OnCompleteDelegate.ExecuteIfBound(ResultInfo); } #undef LOCTEXT_NAMESPACE #endif // SOURCE_CONTROL_WITH_SLATE