Init template repo for UE5

This commit is contained in:
Ben
2025-07-19 00:28:20 +02:00
parent bf2768bbad
commit b203b53bce
36 changed files with 9449 additions and 5 deletions

View File

@@ -0,0 +1,23 @@
{
"FileVersion": 3,
"Version": 316,
"VersionName": "3.16",
"FriendlyName": "Git LFS 2",
"Description": "Git source control management",
"Category": "Source Control",
"CreatedBy": "Project Borealis",
"CreatedByURL": "https://projectborealis.com",
"DocsURL": "https://github.com/ProjectBorealis/UEGitPlugin/blob/release/README.md",
"MarketplaceURL": "",
"SupportURL": "https://github.com/ProjectBorealis/UEGitPlugin/issues",
"EngineVersion": "5.6.0",
"CanContainContent": false,
"Installed": true,
"Modules": [
{
"Name": "GitSourceControl",
"Type": "UncookedOnly",
"LoadingPhase": "Default"
}
]
}

BIN
Plugins/GitSourceControl/Resources/Icon128.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,33 @@
// 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)
using UnrealBuildTool;
public class GitSourceControl : ModuleRules
{
public GitSourceControl(ReadOnlyTargetRules Target) : base(Target)
{
PrivateDependencyModuleNames.AddRange(
new string[] {
"Core",
"CoreUObject",
"Slate",
"SlateCore",
"InputCore",
"DesktopWidgets",
"EditorStyle",
"UnrealEd",
"SourceControl",
"SourceControlWindows",
"Projects"
}
);
if (Target.Version.MajorVersion == 5)
{
PrivateDependencyModuleNames.Add("ToolMenus");
}
}
}

View File

@@ -0,0 +1,113 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "ISourceControlModule.h"
#include "Internationalization/Text.h"
#include "Logging/MessageLog.h"
#include "Logging/TokenizedMessage.h"
#include "Templates/SharedPointer.h"
/**
* A thread safe replacement for FMessageLog which can be called from background threads.
* It only exposes methods from FMessageLog that we would be able to safely delay, such
* as messages. We do not provide any functionality to open error dialogs etc.
* At the moment if we detect a message is being queued when not on the game thread we log
* it instead of sending to the FMessageLog system. In the future we will store the messages
* and marshal them to the GameThread so that they can be displayed as originally intended.
*/
class FTSMessageLog
{
public:
FTSMessageLog() = delete;
FTSMessageLog(const FName& InLogName)
: Log(InLogName)
{}
FTSMessageLog(FTSMessageLog&&) = default;
FTSMessageLog& operator = (FTSMessageLog&&) = default;
FTSMessageLog(const FTSMessageLog&) = delete;
FTSMessageLog& operator = (const FTSMessageLog&) = delete;
~FTSMessageLog() = default;
TSharedRef<FTokenizedMessage> Message(EMessageSeverity::Type InSeverity, const FText& InMessage = FText())
{
if (IsInGameThread())
{
return Log.Message(InSeverity, InMessage);
}
else
{
TSharedRef<FTokenizedMessage> Message = FTokenizedMessage::Create(InSeverity, InMessage);
UE_LOG(LogSourceControl, Display, TEXT("%s"), *Message->ToText().ToString());
return Message;
}
}
TSharedRef<FTokenizedMessage> Error(const FText& InMessage = FText())
{
if (IsInGameThread())
{
return Log.Error(InMessage);
}
else
{
TSharedRef<FTokenizedMessage> Message = FTokenizedMessage::Create(EMessageSeverity::Error, InMessage);
UE_LOG(LogSourceControl, Error, TEXT("%s"), *Message->ToText().ToString());
return Message;
}
}
TSharedRef<FTokenizedMessage> PerformanceWarning(const FText& InMessage = FText())
{
if (IsInGameThread())
{
return Log.PerformanceWarning(InMessage);
}
else
{
TSharedRef<FTokenizedMessage> Message = FTokenizedMessage::Create(EMessageSeverity::PerformanceWarning, InMessage);
UE_LOG(LogSourceControl, Warning, TEXT("%s"), *Message->ToText().ToString());
return Message;
}
}
TSharedRef<FTokenizedMessage> Warning(const FText& InMessage = FText())
{
if (IsInGameThread())
{
return Log.Warning(InMessage);
}
else
{
TSharedRef<FTokenizedMessage> Message = FTokenizedMessage::Create(EMessageSeverity::Warning, InMessage);
UE_LOG(LogSourceControl, Warning, TEXT("%s"), *Message->ToText().ToString());
return Message;
}
}
TSharedRef<FTokenizedMessage> Info(const FText& InMessage = FText())
{
if (IsInGameThread())
{
return Log.Info(InMessage);
}
else
{
TSharedRef<FTokenizedMessage> Message = FTokenizedMessage::Create(EMessageSeverity::Info, InMessage);
UE_LOG(LogSourceControl, Display, TEXT("%s"), *Message->ToText().ToString());
return Message;
}
}
private:
FMessageLog Log;
};

View File

@@ -0,0 +1,4 @@
#include "GitSourceControlChangelist.h"
FGitSourceControlChangelist FGitSourceControlChangelist::WorkingChangelist(TEXT("Working"), true);
FGitSourceControlChangelist FGitSourceControlChangelist::StagedChangelist(TEXT("Staged"), true);

View File

@@ -0,0 +1,79 @@
#pragma once
#include "ISourceControlChangelist.h"
class FGitSourceControlChangelist : public ISourceControlChangelist
{
public:
FGitSourceControlChangelist() = default;
explicit FGitSourceControlChangelist(FString&& InChangelistName, const bool bInInitialized = false)
: ChangelistName(MoveTemp(InChangelistName))
, bInitialized(bInInitialized)
{
}
virtual bool CanDelete() const override
{
return false;
}
bool operator==(const FGitSourceControlChangelist& InOther) const
{
return ChangelistName == InOther.ChangelistName;
}
bool operator!=(const FGitSourceControlChangelist& InOther) const
{
return ChangelistName != InOther.ChangelistName;
}
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
virtual bool IsDefault() const override
{
return ChangelistName == WorkingChangelist.ChangelistName;
}
#endif
void SetInitialized()
{
bInitialized = true;
}
bool IsInitialized() const
{
return bInitialized;
}
void Reset()
{
ChangelistName.Reset();
bInitialized = false;
}
friend FORCEINLINE uint32 GetTypeHash(const FGitSourceControlChangelist& InGitChangelist)
{
return GetTypeHash(InGitChangelist.ChangelistName);
}
FString GetName() const
{
return ChangelistName;
}
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
virtual FString GetIdentifier() const override
{
return ChangelistName;
}
#endif
public:
static FGitSourceControlChangelist WorkingChangelist;
static FGitSourceControlChangelist StagedChangelist;
private:
FString ChangelistName;
bool bInitialized = false;
};
typedef TSharedRef<class FGitSourceControlChangelist, ESPMode::ThreadSafe> FGitSourceControlChangelistRef;

View File

@@ -0,0 +1,74 @@
#include "GitSourceControlChangelistState.h"
#define LOCTEXT_NAMESPACE "GitSourceControl.ChangelistState"
FName FGitSourceControlChangelistState::GetIconName() const
{
// Mimic P4V colors, returning the red icon if there are active file(s), the blue if the changelist is empty or all the files are shelved.
return FName("SourceControl.Changelist");
}
FName FGitSourceControlChangelistState::GetSmallIconName() const
{
return GetIconName();
}
FText FGitSourceControlChangelistState::GetDisplayText() const
{
return FText::FromString(Changelist.GetName());
}
FText FGitSourceControlChangelistState::GetDescriptionText() const
{
return FText::FromString(Description);
}
FText FGitSourceControlChangelistState::GetDisplayTooltip() const
{
return LOCTEXT("Tooltip", "Tooltip");
}
const FDateTime& FGitSourceControlChangelistState::GetTimeStamp() const
{
return TimeStamp;
}
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4
const TArray<FSourceControlStateRef> FGitSourceControlChangelistState::GetFilesStates() const
#else
const TArray<FSourceControlStateRef>& FGitSourceControlChangelistState::GetFilesStates() const
#endif
{
return Files;
}
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4
int32 FGitSourceControlChangelistState::GetFilesStatesNum() const
{
return Files.Num();
}
#endif
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4
const TArray<FSourceControlStateRef> FGitSourceControlChangelistState::GetShelvedFilesStates() const
#else
const TArray<FSourceControlStateRef>& FGitSourceControlChangelistState::GetShelvedFilesStates() const
#endif
{
return ShelvedFiles;
}
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4
int32 FGitSourceControlChangelistState::GetShelvedFilesStatesNum() const
{
return ShelvedFiles.Num();
}
#endif
FSourceControlChangelistRef FGitSourceControlChangelistState::GetChangelist() const
{
FGitSourceControlChangelistRef ChangelistCopy = MakeShareable( new FGitSourceControlChangelist(Changelist));
return StaticCastSharedRef<ISourceControlChangelist>(ChangelistCopy);
}
#undef LOCTEXT_NAMESPACE

View File

@@ -0,0 +1,84 @@
#pragma once
#include "GitSourceControlChangelist.h"
#include "ISourceControlChangelistState.h"
#include "ISourceControlState.h"
class FGitSourceControlChangelistState : public ISourceControlChangelistState
{
public:
explicit FGitSourceControlChangelistState(const FGitSourceControlChangelist& InChangelist,
const FString& InDescription = FString())
: Changelist(InChangelist)
, Description(InDescription)
{
}
explicit FGitSourceControlChangelistState(FGitSourceControlChangelist&& InChangelist,
FString&& InDescription)
: Changelist(MoveTemp(InChangelist))
, Description(MoveTemp(InDescription))
{
}
/**
* Get the name of the icon graphic we should use to display the state in a UI.
* @returns the name of the icon to display
*/
virtual FName GetIconName() const override;
/**
* Get the name of the small icon graphic we should use to display the state in a UI.
* @returns the name of the icon to display
*/
virtual FName GetSmallIconName() const override;
/**
* Get a text representation of the state
* @returns the text to display for this state
*/
virtual FText GetDisplayText() const override;
/**
* Get a text representation of the state
* @returns the text to display for this state
*/
virtual FText GetDescriptionText() const override;
/**
* Get a tooltip to describe this state
* @returns the text to display for this states tooltip
*/
virtual FText GetDisplayTooltip() const override;
/**
* Get the timestamp of the last update that was made to this state.
* @returns the timestamp of the last update
*/
virtual const FDateTime& GetTimeStamp() const override;
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4
virtual const TArray<FSourceControlStateRef> GetFilesStates() const override;
virtual int32 GetFilesStatesNum() const override;
virtual const TArray<FSourceControlStateRef> GetShelvedFilesStates() const override;
virtual int32 GetShelvedFilesStatesNum() const override;
#else
virtual const TArray<FSourceControlStateRef>& GetFilesStates() const override;
virtual const TArray<FSourceControlStateRef>& GetShelvedFilesStates() const override;
#endif
virtual FSourceControlChangelistRef GetChangelist() const override;
public:
FGitSourceControlChangelist Changelist;
FString Description;
TArray<FSourceControlStateRef> Files;
TArray<FSourceControlStateRef> ShelvedFiles;
/** The timestamp of the last update */
FDateTime TimeStamp;
};

View File

@@ -0,0 +1,82 @@
// Copyright (c) 2014-2023 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 "GitSourceControlCommand.h"
#include "Modules/ModuleManager.h"
#include "GitSourceControlModule.h"
#include "GitSourceControlUtils.h"
FGitSourceControlCommand::FGitSourceControlCommand(const TSharedRef<class ISourceControlOperation, ESPMode::ThreadSafe>& InOperation, const TSharedRef<class IGitSourceControlWorker, ESPMode::ThreadSafe>& InWorker, const FSourceControlOperationComplete& InOperationCompleteDelegate)
: Operation(InOperation)
, Worker(InWorker)
, OperationCompleteDelegate(InOperationCompleteDelegate)
, bExecuteProcessed(0)
, bCancelled(0)
, bCommandSuccessful(false)
, bAutoDelete(true)
, Concurrency(EConcurrency::Synchronous)
{
// cache the providers settings here
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
const FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
PathToGitBinary = Provider.GetGitBinaryPath();
bUsingGitLfsLocking = Provider.UsesCheckout();
PathToRepositoryRoot = Provider.GetPathToRepositoryRoot();
PathToGitRoot = Provider.GetPathToGitRoot();
}
void FGitSourceControlCommand::UpdateRepositoryRootIfSubmodule(const TArray<FString>& AbsoluteFilePaths)
{
PathToRepositoryRoot = GitSourceControlUtils::ChangeRepositoryRootIfSubmodule(AbsoluteFilePaths, PathToRepositoryRoot);
}
bool FGitSourceControlCommand::DoWork()
{
bCommandSuccessful = Worker->Execute(*this);
FPlatformAtomics::InterlockedExchange(&bExecuteProcessed, 1);
return bCommandSuccessful;
}
void FGitSourceControlCommand::Abandon()
{
FPlatformAtomics::InterlockedExchange(&bExecuteProcessed, 1);
}
void FGitSourceControlCommand::DoThreadedWork()
{
Concurrency = EConcurrency::Asynchronous;
DoWork();
}
void FGitSourceControlCommand::Cancel()
{
FPlatformAtomics::InterlockedExchange(&bCancelled, 1);
}
bool FGitSourceControlCommand::IsCanceled() const
{
return bCancelled != 0;
}
ECommandResult::Type FGitSourceControlCommand::ReturnResults()
{
// Save any messages that have accumulated
for (const auto& String : ResultInfo.InfoMessages)
{
Operation->AddInfoMessge(FText::FromString(String));
}
for (const auto& String : ResultInfo.ErrorMessages)
{
Operation->AddErrorMessge(FText::FromString(String));
}
// run the completion delegate if we have one bound
ECommandResult::Type Result = bCancelled ? ECommandResult::Cancelled : (bCommandSuccessful ? ECommandResult::Succeeded : ECommandResult::Failed);
OperationCompleteDelegate.ExecuteIfBound(Operation, Result);
return Result;
}

View File

@@ -0,0 +1,128 @@
// Copyright (c) 2014-2023 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)
#pragma once
#include "GitSourceControlChangelist.h"
#include "ISourceControlProvider.h"
#include "Misc/IQueuedWork.h"
/** Accumulated error and info messages for a revision control operation. */
struct FGitSourceControlResultInfo
{
/** Append any messages from another FSourceControlResultInfo, ensuring to keep any already accumulated info. */
void Append(const FGitSourceControlResultInfo& InResultInfo)
{
InfoMessages.Append(InResultInfo.InfoMessages);
ErrorMessages.Append(InResultInfo.ErrorMessages);
}
/** Info and/or warning message storage */
TArray<FString> InfoMessages;
/** Potential error message storage */
TArray<FString> ErrorMessages;
};
/**
* Used to execute Git commands multi-threaded.
*/
class FGitSourceControlCommand : public IQueuedWork
{
public:
FGitSourceControlCommand(const TSharedRef<class ISourceControlOperation, ESPMode::ThreadSafe>& InOperation, const TSharedRef<class IGitSourceControlWorker, ESPMode::ThreadSafe>& InWorker, const FSourceControlOperationComplete& InOperationCompleteDelegate = FSourceControlOperationComplete());
/**
* Modify the repo root if all selected files are in a plugin subfolder, and the plugin subfolder is a git repo
* This supports the case where each plugin is a sub module
*/
void UpdateRepositoryRootIfSubmodule(const TArray<FString>& AbsoluteFilePaths);
/**
* This is where the real thread work is done. All work that is done for
* this queued object should be done from within the call to this function.
*/
bool DoWork();
/**
* Tells the queued work that it is being abandoned so that it can do
* per object clean up as needed. This will only be called if it is being
* abandoned before completion. NOTE: This requires the object to delete
* itself using whatever heap it was allocated in.
*/
virtual void Abandon() override;
/**
* This method is also used to tell the object to cleanup but not before
* the object has finished it's work.
*/
virtual void DoThreadedWork() override;
/** Attempt to cancel the operation */
void Cancel();
/** Is the operation canceled? */
bool IsCanceled() const;
/** Save any results and call any registered callbacks. */
ECommandResult::Type ReturnResults();
public:
/** Path to the Git binary */
FString PathToGitBinary;
/** Path to the root of the Unreal revision control repository: usually the ProjectDir */
FString PathToRepositoryRoot;
/** Path to the root of the Git repository: can be the ProjectDir itself, or any parent directory (found by the "Connect" operation) */
FString PathToGitRoot;
/** Tell if using the Git LFS file Locking workflow */
bool bUsingGitLfsLocking;
/** Operation we want to perform - contains outward-facing parameters & results */
TSharedRef<class ISourceControlOperation, ESPMode::ThreadSafe> Operation;
/** The object that will actually do the work */
TSharedRef<class IGitSourceControlWorker, ESPMode::ThreadSafe> Worker;
/** Delegate to notify when this operation completes */
FSourceControlOperationComplete OperationCompleteDelegate;
/**If true, this command has been processed by the revision control thread*/
volatile int32 bExecuteProcessed;
/**If true, this command has been cancelled*/
volatile int32 bCancelled;
/**If true, the revision control command succeeded*/
bool bCommandSuccessful;
/** Current Commit full SHA1 */
FString CommitId;
/** Current Commit description's Summary */
FString CommitSummary;
/** If true, this command will be automatically cleaned up in Tick() */
bool bAutoDelete;
/** Whether we are running multi-treaded or not*/
EConcurrency::Type Concurrency;
/** Files to perform this operation on */
TArray<FString> Files;
/** Changelist to perform this operation on */
FGitSourceControlChangelist Changelist;
/** Potential error, warning and info message storage */
FGitSourceControlResultInfo ResultInfo;
/** Branch names for status queries */
TArray< FString > StatusBranchNames;
};

View File

@@ -0,0 +1,44 @@
// Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
#include "GitSourceControlConsole.h"
#include "HAL/IConsoleManager.h"
#include "ISourceControlModule.h"
#include "GitSourceControlModule.h"
#include "GitSourceControlUtils.h"
// Auto-registered console commands:
// No re-register on hot reload, and unregistered only once on editor shutdown.
static FAutoConsoleCommand g_executeGitConsoleCommand(TEXT("git"),
TEXT("Git Command Line Interface.\n")
TEXT("Run any 'git' command directly from the Unreal Editor Console.\n")
TEXT("Type 'git help' to get a command list."),
FConsoleCommandWithArgsDelegate::CreateStatic(&GitSourceControlConsole::ExecuteGitConsoleCommand));
void GitSourceControlConsole::ExecuteGitConsoleCommand(const TArray<FString>& a_args)
{
FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked<FGitSourceControlModule>("GitSourceControl");
const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
const FString& RepositoryRoot = GitSourceControl.GetProvider().GetPathToRepositoryRoot();
// The first argument is the command to send to git, the following ones are forwarded as parameters for the command
TArray<FString> Parameters = a_args;
FString Command;
if (a_args.Num() > 0)
{
Command = a_args[0];
Parameters.RemoveAt(0);
}
else
{
// If no command is provided, use "help" to emulate the behavior of the git CLI
Command = TEXT("help");
}
FString Results;
FString Errors;
GitSourceControlUtils::RunCommandInternalRaw(Command, PathToGitBinary, RepositoryRoot, Parameters, TArray<FString>(), Results, Errors);
UE_LOG(LogSourceControl, Log, TEXT("Output:\n%s"), *Results);
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
#pragma once
#include "CoreMinimal.h"
/**
* Editor only console commands.
*
* Such commands can be executed from the editor output log window, but also from command line arguments,
* from Editor Blueprints utilities, or from C++ Code using. eg. GEngine->Exec("git status Content/");
*/
class GitSourceControlConsole
{
public:
// Git Command Line Interface: Run 'git' commands directly from the Unreal Editor Console.
static void ExecuteGitConsoleCommand(const TArray<FString>& a_args);
};

View File

@@ -0,0 +1,614 @@
// 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<SNotificationItem> 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<FLevelEditorModule>(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<FLevelEditorModule>("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<UPackage*> 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<FString> ParametersStatus{"--porcelain --untracked-files=no"};
TArray<FString> InfoMessages;
TArray<FString> 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<FString> 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<FString> ParametersStash{ "pop" };
TArray<FString> InfoMessages;
TArray<FString> 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<FSync, ESPMode::ThreadSafe> SyncOperation = ISourceControlOperation::Create<FSync>();
#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<FLevelEditorModule>("LevelEditor");
FSourceControlWindows::ChoosePackagesToCheckIn(nullptr);
}
void FGitSourceControlMenu::PushClicked()
{
if (!OperationInProgressNotification.IsValid())
{
// Launch a "Push" Operation
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
TSharedRef<FCheckIn, ESPMode::ThreadSafe> PushOperation = ISourceControlOperation::Create<FCheckIn>();
#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<FString> Filenames {
FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()),
FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir()),
FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath())
};
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
FSourceControlOperationRef Operation = ISourceControlOperation::Create<FUpdateStatus>();
#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<FString> PackageNames;
TArray<UPackage*> LoadedPackages;
TMap<FString, FSourceControlStatePtr> PackageStates;
FEditorFileUtils::FindAllSubmittablePackageFiles(PackageStates, true);
for (TMap<FString, FSourceControlStatePtr>::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<FRevert, ESPMode::ThreadSafe> RevertOperation = ISourceControlOperation::Create<FRevert>();
#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<FUpdateStatus>(), FSourceControlChangelistPtr(), FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous);
#else
Provider.Execute(ISourceControlOperation::Create<FUpdateStatus>(), FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous);
#endif
}
void FGitSourceControlMenu::RefreshClicked()
{
if (!OperationInProgressNotification.IsValid())
{
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
// Launch an "GitFetch" Operation
TSharedRef<FGitFetch, ESPMode::ThreadSafe> RefreshOperation = ISourceControlOperation::Create<FGitFetch>();
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<FExtender> FGitSourceControlMenu::OnExtendLevelEditorViewMenu(const TSharedRef<FUICommandList> CommandList)
{
TSharedRef<FExtender> Extender(new FExtender());
Extender->AddMenuExtension(
"SourceControlActions",
EExtensionHook::After,
nullptr,
FMenuExtensionDelegate::CreateRaw(this, &FGitSourceControlMenu::AddMenuExtension));
return Extender;
}
#endif
#undef LOCTEXT_NAMESPACE

View File

@@ -0,0 +1,69 @@
// 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)
#pragma once
#include "CoreMinimal.h"
#include "ISourceControlProvider.h"
#include "Runtime/Launch/Resources/Version.h"
struct FToolMenuSection;
class FMenuBuilder;
/** Git extension of the Revision Control toolbar menu */
class FGitSourceControlMenu
{
public:
void Register();
void Unregister();
/** This functions will be bound to appropriate Command. */
void CommitClicked();
void PushClicked();
void SyncClicked();
void RevertClicked();
void RefreshClicked();
protected:
static void RevertAllCallback(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult);
static void RevertAllCancelled(FSourceControlOperationRef InOperation);
private:
bool HaveRemoteUrl() const;
bool SaveDirtyPackages();
bool StashAwayAnyModifications();
void ReApplyStashedModifications();
#if ENGINE_MAJOR_VERSION >= 5
void AddMenuExtension(FToolMenuSection& Builder);
#else
void AddMenuExtension(FMenuBuilder& Builder);
TSharedRef<class FExtender> OnExtendLevelEditorViewMenu(const TSharedRef<class FUICommandList> CommandList);
#endif
static void DisplayInProgressNotification(const FText& InOperationInProgressString);
static void RemoveInProgressNotification();
static void DisplaySucessNotification(const FName& InOperationName);
static void DisplayFailureNotification(const FName& InOperationName);
private:
#if ENGINE_MAJOR_VERSION < 5
FDelegateHandle ViewMenuExtenderHandle;
#endif
/** Was there a need to stash away modifications before Sync? */
bool bStashMadeBeforeSync;
/** Loaded packages to reload after a Sync or Revert operation */
TArray<UPackage*> PackagesToReload;
/** Current revision control operation from extended menu if any */
static TWeakPtr<class SNotificationItem> OperationInProgressNotification;
/** Delegate called when a revision control operation has completed */
void OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult);
};

View File

@@ -0,0 +1,241 @@
// 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 "GitSourceControlModule.h"
#include "AssetToolsModule.h"
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
#include "Styling/AppStyle.h"
#else
#include "EditorStyleSet.h"
#endif
#include "Misc/App.h"
#include "Modules/ModuleManager.h"
#include "Features/IModularFeatures.h"
#include "ContentBrowserModule.h"
#include "ContentBrowserDelegates.h"
#include "GitSourceControlOperations.h"
#include "GitSourceControlUtils.h"
#include "ISourceControlModule.h"
#include "SourceControlHelpers.h"
#include "Framework/Commands/UIAction.h"
#include "Framework/MultiBox/MultiBoxExtender.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#define LOCTEXT_NAMESPACE "GitSourceControl"
TArray<FString> FGitSourceControlModule::EmptyStringArray;
template<typename Type>
static TSharedRef<IGitSourceControlWorker, ESPMode::ThreadSafe> CreateWorker()
{
return MakeShareable( new Type() );
}
void FGitSourceControlModule::StartupModule()
{
// Register our operations (implemented in GitSourceControlOperations.cpp by subclassing from Engine\Source\Developer\SourceControl\Public\SourceControlOperations.h)
GitSourceControlProvider.RegisterWorker( "Connect", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitConnectWorker> ) );
// Note: this provider uses the "CheckOut" command only with Git LFS 2 "lock" command, since Git itself has no lock command (all tracked files in the working copy are always already checked-out).
GitSourceControlProvider.RegisterWorker( "CheckOut", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitCheckOutWorker> ) );
GitSourceControlProvider.RegisterWorker( "UpdateStatus", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitUpdateStatusWorker> ) );
GitSourceControlProvider.RegisterWorker( "MarkForAdd", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitMarkForAddWorker> ) );
GitSourceControlProvider.RegisterWorker( "Delete", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitDeleteWorker> ) );
GitSourceControlProvider.RegisterWorker( "Revert", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitRevertWorker> ) );
GitSourceControlProvider.RegisterWorker( "Sync", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitSyncWorker> ) );
GitSourceControlProvider.RegisterWorker( "Fetch", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitFetchWorker> ) );
GitSourceControlProvider.RegisterWorker( "CheckIn", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitCheckInWorker> ) );
GitSourceControlProvider.RegisterWorker( "Copy", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitCopyWorker> ) );
GitSourceControlProvider.RegisterWorker( "Resolve", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitResolveWorker> ) );
GitSourceControlProvider.RegisterWorker( "MoveToChangelist", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitMoveToChangelistWorker> ) );
GitSourceControlProvider.RegisterWorker( "UpdateChangelistsStatus", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitUpdateStagingWorker> ) );
// load our settings
GitSourceControlSettings.LoadSettings();
// Bind our revision control provider to the editor
IModularFeatures::Get().RegisterModularFeature( "SourceControl", &GitSourceControlProvider );
FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked<FContentBrowserModule>("ContentBrowser");
#if ENGINE_MAJOR_VERSION >= 5
// Register ContentBrowserDelegate Handles for UE5 EA
// At the time of writing this UE5 is in Early Access and has no support for revision control yet. So instead we hook into the content browser..
// .. and force a state update on the next tick for revision control. Usually the contentbrowser assets will request this themselves, but that's not working
// Values here are 1 or 2 based on whether the change can be done immediately or needs to be delayed as unreal needs to work through its internal delegates first
// >> Technically you wouldn't need to use `GetOnAssetSelectionChanged` -- but it's there as a safety mechanism. States aren't forceupdated for the first path that loads
// >> Making sure we force an update on selection change that acts like a just in case other measures fail
CbdHandle_OnFilterChanged = ContentBrowserModule.GetOnFilterChanged().AddLambda( [this]( const FARFilter&, bool ) { GitSourceControlProvider.TicksUntilNextForcedUpdate = 2; } );
CbdHandle_OnSearchBoxChanged = ContentBrowserModule.GetOnSearchBoxChanged().AddLambda( [this]( const FText&, bool ){ GitSourceControlProvider.TicksUntilNextForcedUpdate = 1; } );
CbdHandle_OnAssetSelectionChanged = ContentBrowserModule.GetOnAssetSelectionChanged().AddLambda( [this]( const TArray<FAssetData>&, bool ) { GitSourceControlProvider.TicksUntilNextForcedUpdate = 1; } );
CbdHandle_OnAssetPathChanged = ContentBrowserModule.GetOnAssetPathChanged().AddLambda( [this]( const FString& ) { GitSourceControlProvider.TicksUntilNextForcedUpdate = 2; } );
#endif
TArray<FContentBrowserMenuExtender_SelectedAssets>& CBAssetMenuExtenderDelegates = ContentBrowserModule.GetAllAssetViewContextMenuExtenders();
CBAssetMenuExtenderDelegates.Add(FContentBrowserMenuExtender_SelectedAssets::CreateRaw( this, &FGitSourceControlModule::OnExtendContentBrowserAssetSelectionMenu ));
CbdHandle_OnExtendAssetSelectionMenu = CBAssetMenuExtenderDelegates.Last().GetHandle();
}
void FGitSourceControlModule::ShutdownModule()
{
// shut down the provider, as this module is going away
GitSourceControlProvider.Close();
// unbind provider from editor
IModularFeatures::Get().UnregisterModularFeature("SourceControl", &GitSourceControlProvider);
// Unregister ContentBrowserDelegate Handles
FContentBrowserModule & ContentBrowserModule = FModuleManager::Get().LoadModuleChecked< FContentBrowserModule >( "ContentBrowser" );
#if ENGINE_MAJOR_VERSION >= 5
ContentBrowserModule.GetOnFilterChanged().Remove( CbdHandle_OnFilterChanged );
ContentBrowserModule.GetOnSearchBoxChanged().Remove( CbdHandle_OnSearchBoxChanged );
ContentBrowserModule.GetOnAssetSelectionChanged().Remove( CbdHandle_OnAssetSelectionChanged );
ContentBrowserModule.GetOnAssetPathChanged().Remove( CbdHandle_OnAssetPathChanged );
#endif
TArray<FContentBrowserMenuExtender_SelectedAssets>& CBAssetMenuExtenderDelegates = ContentBrowserModule.GetAllAssetViewContextMenuExtenders();
CBAssetMenuExtenderDelegates.RemoveAll([ &ExtenderDelegateHandle = CbdHandle_OnExtendAssetSelectionMenu ]( const FContentBrowserMenuExtender_SelectedAssets& Delegate ) {
return Delegate.GetHandle() == ExtenderDelegateHandle;
});
}
void FGitSourceControlModule::SaveSettings()
{
if (FApp::IsUnattended() || IsRunningCommandlet())
{
return;
}
GitSourceControlSettings.SaveSettings();
}
void FGitSourceControlModule::SetLastErrors(const TArray<FText>& InErrors)
{
FGitSourceControlModule* Module = FModuleManager::GetModulePtr<FGitSourceControlModule>("GitSourceControl");
if (Module)
{
Module->GetProvider().SetLastErrors(InErrors);
}
}
TSharedRef<FExtender> FGitSourceControlModule::OnExtendContentBrowserAssetSelectionMenu(const TArray<FAssetData>& SelectedAssets)
{
TSharedRef<FExtender> Extender(new FExtender());
Extender->AddMenuExtension(
"AssetSourceControlActions",
EExtensionHook::After,
nullptr,
FMenuExtensionDelegate::CreateRaw( this, &FGitSourceControlModule::CreateGitContentBrowserAssetMenu, SelectedAssets )
);
return Extender;
}
void FGitSourceControlModule::CreateGitContentBrowserAssetMenu(FMenuBuilder& MenuBuilder, const TArray<FAssetData> SelectedAssets)
{
if (!FGitSourceControlModule::Get().GetProvider().GetStatusBranchNames().Num())
{
return;
}
const TArray<FString>& StatusBranchNames = FGitSourceControlModule::Get().GetProvider().GetStatusBranchNames();
const FString& BranchName = StatusBranchNames[0];
MenuBuilder.AddMenuEntry(
FText::Format(LOCTEXT("StatusBranchDiff", "Diff against status branch"), FText::FromString(BranchName)),
FText::Format(LOCTEXT("StatusBranchDiffDesc", "Compare this asset to the latest status branch version"), FText::FromString(BranchName)),
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
FSlateIcon(FAppStyle::GetAppStyleSetName(), "SourceControl.Actions.Diff"),
#else
FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Diff"),
#endif
FUIAction(FExecuteAction::CreateRaw( this, &FGitSourceControlModule::DiffAssetAgainstGitOriginBranch, SelectedAssets, BranchName ))
);
}
void FGitSourceControlModule::DiffAssetAgainstGitOriginBranch(const TArray<FAssetData> SelectedAssets, FString BranchName) const
{
for (int32 AssetIdx = 0; AssetIdx < SelectedAssets.Num(); AssetIdx++)
{
// Get the actual asset (will load it)
const FAssetData& AssetData = SelectedAssets[AssetIdx];
if (UObject* CurrentObject = AssetData.GetAsset())
{
const FString PackagePath = AssetData.PackageName.ToString();
const FString PackageName = AssetData.AssetName.ToString();
DiffAgainstOriginBranch(CurrentObject, PackagePath, PackageName, BranchName);
}
}
}
void FGitSourceControlModule::DiffAgainstOriginBranch( UObject * InObject, const FString & InPackagePath, const FString & InPackageName, const FString & BranchName ) const
{
check(InObject);
const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("GitSourceControl");
const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
const FString& PathToRepositoryRoot = GitSourceControl.GetProvider().GetPathToRepositoryRoot();
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
const FAssetToolsModule& AssetToolsModule = FModuleManager::GetModuleChecked<FAssetToolsModule>("AssetTools");
// Get the SCC state
const FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(SourceControlHelpers::PackageFilename(InPackagePath), EStateCacheUsage::Use);
// If we have an asset and its in SCC..
if (SourceControlState.IsValid() && InObject != nullptr && SourceControlState->IsSourceControlled())
{
// Get the file name of package
FString RelativeFileName;
#if ENGINE_MAJOR_VERSION >= 5
if (FPackageName::DoesPackageExist(InPackagePath, &RelativeFileName))
#else
if (FPackageName::DoesPackageExist(InPackagePath, nullptr, &RelativeFileName))
#endif
{
// if(SourceControlState->GetHistorySize() > 0)
{
TArray<FString> Errors;
const auto& Revision = GitSourceControlUtils::GetOriginRevisionOnBranch(PathToGitBinary, PathToRepositoryRoot, RelativeFileName, Errors, BranchName);
check(Revision.IsValid());
FString TempFileName;
if (Revision->Get(TempFileName))
{
// Try and load that package
UPackage* TempPackage = LoadPackage(nullptr, *TempFileName, LOAD_ForDiff | LOAD_DisableCompileOnLoad);
if (TempPackage != nullptr)
{
// Grab the old asset from that old package
UObject* OldObject = FindObject<UObject>(TempPackage, *InPackageName);
if (OldObject != nullptr)
{
/* Set the revision information*/
FRevisionInfo OldRevision;
OldRevision.Changelist = Revision->GetCheckInIdentifier();
OldRevision.Date = Revision->GetDate();
OldRevision.Revision = Revision->GetRevision();
FRevisionInfo NewRevision;
NewRevision.Revision = TEXT("");
AssetToolsModule.Get().DiffAssets(OldObject, InObject, OldRevision, NewRevision);
}
}
}
}
}
}
}
IMPLEMENT_MODULE( FGitSourceControlModule, GitSourceControl );
#undef LOCTEXT_NAMESPACE

View File

@@ -0,0 +1,160 @@
// 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)
#pragma once
#include "Modules/ModuleInterface.h"
#include "Modules/ModuleManager.h"
#include "GitSourceControlSettings.h"
#include "GitSourceControlProvider.h"
struct FAssetData;
class FExtender;
/**
UEGitPlugin is a simple Git Revision Control Plugin for Unreal Engine
Written and contributed by Sebastien Rombauts (sebastien.rombauts@gmail.com)
### Supported features
- initialize a new Git local repository ('git init') to manage your UE Game Project
- can also create an appropriate .gitignore file as part of initialization
- can also create a .gitattributes file to enable Git LFS (Large File System) as part of initialization
- can also make the initial commit, with custom multi-line message
- can also configure the default remote origin URL
- display status icons to show modified/added/deleted/untracked files
- show history of a file
- visual diff of a blueprint against depot or between previous versions of a file
- revert modifications of a file
- add, delete, rename a file
- checkin/commit a file (cannot handle atomically more than 50 files)
- migrate an asset between two projects if both are using Git
- solve a merge conflict on a blueprint
- show current branch name in status text
- Sync to Pull (rebase) the current branch
- Git LFS (Github, Gitlab, Bitbucket) is working with Git 2.10+ under Windows
- Git LFS 2 File Locking is working with Git 2.10+ and Git LFS 2.0.0
- Windows, Mac and Linux
### TODO
1. configure the name of the remote instead of default "origin"
### TODO LFS 2.x File Locking
Known issues:
0. False error logs after a successful push:
Use "TODO LFS" in the code to track things left to do/improve/refactor:
2. Implement FGitSourceControlProvider::bWorkingOffline like the SubversionSourceControl plugin
3. Trying to deactivate Git LFS 2 file locking afterward on the "Login to Revision Control" (Connect/Configure) screen
is not working after Git LFS 2 has switched "read-only" flag on files (which needs the Checkout operation to be editable)!
- temporarily deactivating locks may be required if we want to be able to work while not connected (do we really need this ???)
- does Git LFS have a command to do this deactivation ?
- perhaps should we rely on detection of such flags to detect LFS 2 usage (ie. the need to do a checkout)
- see SubversionSourceControl plugin that deals with such flags
- this would need a rework of the way the "bIsUsingFileLocking" is propagated, since this would no more be a configuration (or not only) but a file state
- else we should at least revert those read-only flags when going out of "Lock mode"
### What *cannot* be done presently
- Branch/Merge are not in the current Editor workflow
- Amend a commit is not in the current Editor workflow
- Configure user name & email ('git config user.name' & git config user.email')
### Known issues
- the Editor does not show deleted files (only when deleted externally?)
- the Editor does not show missing files
- missing localization for git specific messages
- renaming a Blueprint in Editor leaves a redirector file, AND modify too much the asset to enable git to track its history through renaming
- standard Editor commit dialog asks if user wants to "Keep Files Checked Out" => no use for Git or Mercurial CanCheckOut()==false
*/
class FGitSourceControlModule : public IModuleInterface
{
public:
/** IModuleInterface implementation */
virtual void StartupModule() override;
virtual void ShutdownModule() override;
/** Access the Git revision control settings */
FGitSourceControlSettings& AccessSettings()
{
return GitSourceControlSettings;
}
const FGitSourceControlSettings& AccessSettings() const
{
return GitSourceControlSettings;
}
/** Save the Git revision control settings */
void SaveSettings();
/** Access the Git revision control provider */
FGitSourceControlProvider& GetProvider()
{
return GitSourceControlProvider;
}
const FGitSourceControlProvider& GetProvider() const
{
return GitSourceControlProvider;
}
static const TArray<FString>& GetEmptyStringArray()
{
return EmptyStringArray;
}
/**
* Singleton-like access to this module's interface. This is just for convenience!
* Beware of calling this during the shutdown phase, though. Your module might have been unloaded already.
*
* @return Returns singleton instance, loading the module on demand if needed
*/
static inline FGitSourceControlModule& Get()
{
return FModuleManager::Get().LoadModuleChecked< FGitSourceControlModule >("GitSourceControl");
}
static inline FGitSourceControlModule* GetThreadSafe()
{
IModuleInterface* ModulePtr = FModuleManager::Get().GetModule("GitSourceControl");
if (!ModulePtr)
{
// Main thread should never have this unloaded.
check(!IsInGameThread());
return nullptr;
}
return static_cast<FGitSourceControlModule*>(ModulePtr);
}
/** Set list of error messages that occurred after last git command */
static void SetLastErrors(const TArray<FText>& InErrors);
private:
TSharedRef<FExtender> OnExtendContentBrowserAssetSelectionMenu(const TArray<FAssetData>& SelectedAssets);
void CreateGitContentBrowserAssetMenu(FMenuBuilder& MenuBuilder, const TArray<FAssetData> SelectedAssets);
void DiffAssetAgainstGitOriginBranch(const TArray<FAssetData> SelectedAssets, FString BranchName) const;
void DiffAgainstOriginBranch(UObject* InObject, const FString& InPackagePath, const FString& InPackageName, const FString& BranchName) const;
/** The one and only Git revision control provider */
FGitSourceControlProvider GitSourceControlProvider;
/** The settings for Git revision control */
FGitSourceControlSettings GitSourceControlSettings;
static TArray<FString> EmptyStringArray;
#if ENGINE_MAJOR_VERSION >= 5
// ContentBrowserDelegate Handles
FDelegateHandle CbdHandle_OnFilterChanged;
FDelegateHandle CbdHandle_OnSearchBoxChanged;
FDelegateHandle CbdHandle_OnAssetSelectionChanged;
FDelegateHandle CbdHandle_OnSourcesViewChanged;
FDelegateHandle CbdHandle_OnAssetPathChanged;
#endif
FDelegateHandle CbdHandle_OnExtendAssetSelectionMenu;
};

View File

@@ -0,0 +1,909 @@
// 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 "GitSourceControlOperations.h"
#include "Misc/Paths.h"
#include "Modules/ModuleManager.h"
#include "SourceControlOperations.h"
#include "ISourceControlModule.h"
#include "GitSourceControlModule.h"
#include "GitSourceControlCommand.h"
#include "GitSourceControlUtils.h"
#include "SourceControlHelpers.h"
#include "Logging/MessageLog.h"
#include "Misc/MessageDialog.h"
#include "HAL/PlatformProcess.h"
#include "GenericPlatform/GenericPlatformFile.h"
#if ENGINE_MAJOR_VERSION >= 5
#include "HAL/PlatformFileManager.h"
#else
#include "HAL/PlatformFilemanager.h"
#endif
#include <thread>
#define LOCTEXT_NAMESPACE "GitSourceControl"
FName FGitConnectWorker::GetName() const
{
return "Connect";
}
bool FGitConnectWorker::Execute(FGitSourceControlCommand& InCommand)
{
// The connect worker checks if we are connected to the remote server.
check(InCommand.Operation->GetName() == GetName());
TSharedRef<FConnect, ESPMode::ThreadSafe> Operation = StaticCastSharedRef<FConnect>(InCommand.Operation);
// Skip login operations, since Git does not have to login.
// It's not a big deal for async commands though, so let those go through.
// More information: this is a heuristic for cases where UE is trying to create
// a valid Perforce connection as a side effect for the connect worker. For Git,
// the connect worker has no side effects. It is simply a query to retrieve information
// to be displayed to the user, like in the revision control settings or on init.
// Therefore, there is no need for synchronously establishing a connection if not there.
if (InCommand.Concurrency == EConcurrency::Synchronous)
{
InCommand.bCommandSuccessful = true;
return true;
}
// Check Git availability
// We already know that Git is available if PathToGitBinary is not empty, since it is validated then.
if (InCommand.PathToGitBinary.IsEmpty())
{
const FText& NotFound = LOCTEXT("GitNotFound", "Failed to enable Git revision control. You need to install Git and ensure the plugin has a valid path to the git executable.");
InCommand.ResultInfo.ErrorMessages.Add(NotFound.ToString());
Operation->SetErrorText(NotFound);
InCommand.bCommandSuccessful = false;
return false;
}
// Get default branch: git remote show
TArray<FString> Parameters {
TEXT("-h"), // Only limit to branches
TEXT("-q") // Skip printing out remote URL, we don't use it
};
// Check if remote matches our refs.
// Could be useful in the future, but all we want to know right now is if connection is up.
// Parameters.Add("--exit-code");
TArray<FString> InfoMessages;
TArray<FString> ErrorMessages;
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("ls-remote"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages);
if (!InCommand.bCommandSuccessful)
{
const FText& NotFound = LOCTEXT("GitRemoteFailed", "Failed Git remote connection. Ensure your repo is initialized, and check your connection to the Git host.");
InCommand.ResultInfo.ErrorMessages.Add(NotFound.ToString());
Operation->SetErrorText(NotFound);
}
// TODO: always return true, and enter an offline mode if could not connect to remote
return InCommand.bCommandSuccessful;
}
bool FGitConnectWorker::UpdateStates() const
{
return false;
}
FName FGitCheckOutWorker::GetName() const
{
return "CheckOut";
}
bool FGitCheckOutWorker::Execute(FGitSourceControlCommand& InCommand)
{
// If we have nothing to process, exit immediately
if (InCommand.Files.Num() == 0)
{
return true;
}
check(InCommand.Operation->GetName() == GetName());
if (!InCommand.bUsingGitLfsLocking)
{
InCommand.bCommandSuccessful = false;
return InCommand.bCommandSuccessful;
}
// lock files: execute the LFS command on relative filenames
const TArray<FString>& RelativeFiles = GitSourceControlUtils::RelativeFilenames(InCommand.Files, InCommand.PathToGitRoot);
const TArray<FString>& LockableRelativeFiles = RelativeFiles.FilterByPredicate(GitSourceControlUtils::IsFileLFSLockable);
if (LockableRelativeFiles.Num() < 1)
{
InCommand.bCommandSuccessful = true;
return InCommand.bCommandSuccessful;
}
const bool bSuccess = GitSourceControlUtils::RunLFSCommand(TEXT("lock"), InCommand.PathToGitRoot, InCommand.PathToGitBinary, FGitSourceControlModule::GetEmptyStringArray(), LockableRelativeFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
InCommand.bCommandSuccessful = bSuccess;
const FString& LockUser = FGitSourceControlModule::Get().GetProvider().GetLockUser();
if (bSuccess)
{
TArray<FString> AbsoluteFiles;
for (const auto& RelativeFile : RelativeFiles)
{
FString AbsoluteFile = FPaths::Combine(InCommand.PathToGitRoot, RelativeFile);
FGitLockedFilesCache::AddLockedFile(AbsoluteFile, LockUser);
FPaths::NormalizeFilename(AbsoluteFile);
AbsoluteFiles.Add(AbsoluteFile);
}
GitSourceControlUtils::CollectNewStates(AbsoluteFiles, States, EFileState::Unset, ETreeState::Unset, ELockState::Locked);
for (auto& State : States)
{
State.Value.LockUser = LockUser;
}
}
return InCommand.bCommandSuccessful;
}
bool FGitCheckOutWorker::UpdateStates() const
{
return GitSourceControlUtils::UpdateCachedStates(States);
}
static FText ParseCommitResults(const TArray<FString>& InResults)
{
if (InResults.Num() >= 1)
{
const FString& FirstLine = InResults[0];
return FText::Format(LOCTEXT("CommitMessage", "Commited {0}."), FText::FromString(FirstLine));
}
return LOCTEXT("CommitMessageUnknown", "Submitted revision.");
}
FName FGitCheckInWorker::GetName() const
{
return "CheckIn";
}
const FText EmptyCommitMsg;
bool FGitCheckInWorker::Execute(FGitSourceControlCommand& InCommand)
{
check(InCommand.Operation->GetName() == GetName());
TSharedRef<FCheckIn, ESPMode::ThreadSafe> Operation = StaticCastSharedRef<FCheckIn>(InCommand.Operation);
// make a temp file to place our commit message in
bool bDoCommit = InCommand.Files.Num() > 0;
const FText& CommitMsg = bDoCommit ? Operation->GetDescription() : EmptyCommitMsg;
FGitScopedTempFile CommitMsgFile(CommitMsg);
if (CommitMsgFile.GetFilename().Len() > 0)
{
FGitSourceControlProvider& Provider = FGitSourceControlModule::Get().GetProvider();
if (bDoCommit)
{
FString ParamCommitMsgFilename = TEXT("--file=\"");
ParamCommitMsgFilename += FPaths::ConvertRelativePathToFull(CommitMsgFile.GetFilename());
ParamCommitMsgFilename += TEXT("\"");
TArray<FString> CommitParameters {ParamCommitMsgFilename};
const TArray<FString>& FilesToCommit = GitSourceControlUtils::RelativeFilenames(InCommand.Files, InCommand.PathToRepositoryRoot);
// If no files were committed, this is false, so we treat it as if we never wanted to commit in the first place.
bDoCommit = GitSourceControlUtils::RunCommit(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, CommitParameters,
FilesToCommit, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
}
// If we commit, we can push up the deleted state to gone
if (bDoCommit)
{
// Remove any deleted files from status cache
TArray<TSharedRef<ISourceControlState, ESPMode::ThreadSafe>> LocalStates;
Provider.GetState(InCommand.Files, LocalStates, EStateCacheUsage::Use);
for (const auto& State : LocalStates)
{
if (State->IsDeleted())
{
Provider.RemoveFileFromCache(State->GetFilename());
}
}
Operation->SetSuccessMessage(ParseCommitResults(InCommand.ResultInfo.InfoMessages));
const FString& Message = (InCommand.ResultInfo.InfoMessages.Num() > 0) ? InCommand.ResultInfo.InfoMessages[0] : TEXT("");
UE_LOG(LogSourceControl, Log, TEXT("commit successful: %s"), *Message);
GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary);
}
// Collect difference between the remote and what we have on top of remote locally. This is to handle unpushed commits other than the one we just did.
// Doesn't matter that we're not synced. Because our local branch is always based on the remote.
TArray<FString> CommittedFiles;
FString BranchName;
bool bDiffSuccess;
if (GitSourceControlUtils::GetRemoteBranchName(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, BranchName))
{
TArray<FString> Parameters {"--name-only", FString::Printf(TEXT("%s...HEAD"), *BranchName), "--"};
bDiffSuccess = GitSourceControlUtils::RunCommand(TEXT("diff"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters,
FGitSourceControlModule::GetEmptyStringArray(), CommittedFiles, InCommand.ResultInfo.ErrorMessages);
}
else
{
// Get all non-remote commits and list out their files
TArray<FString> Parameters {"--branches", "--not" "--remotes", "--name-only", "--pretty="};
bDiffSuccess = GitSourceControlUtils::RunCommand(TEXT("log"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters, FGitSourceControlModule::GetEmptyStringArray(), CommittedFiles, InCommand.ResultInfo.ErrorMessages);
// Dedup files list between commits
CommittedFiles = TSet<FString>{CommittedFiles}.Array();
}
bool bUnpushedFiles;
TSet<FString> FilesToCheckIn {InCommand.Files};
if (bDiffSuccess)
{
// Only push if we have a difference (any commits at all, not just the one we just did)
bUnpushedFiles = CommittedFiles.Num() > 0;
CommittedFiles = GitSourceControlUtils::AbsoluteFilenames(CommittedFiles, InCommand.PathToRepositoryRoot);
FilesToCheckIn.Append(CommittedFiles.FilterByPredicate(GitSourceControlUtils::IsFileLFSLockable));
}
else
{
// Be cautious, try pushing anyway
bUnpushedFiles = true;
}
TArray<FString> PulledFiles;
// If we have unpushed files, push
if (bUnpushedFiles)
{
// TODO: configure remote
TArray<FString> PushParameters {TEXT("-u"), TEXT("origin"), TEXT("HEAD")};
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("push"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot,
PushParameters, FGitSourceControlModule::GetEmptyStringArray(),
InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
if (!InCommand.bCommandSuccessful)
{
// if out of date, pull first, then try again
bool bWasOutOfDate = false;
for (const auto& PushError : InCommand.ResultInfo.ErrorMessages)
{
if ((PushError.Contains(TEXT("[rejected]")) && (PushError.Contains(TEXT("non-fast-forward")) || PushError.Contains(TEXT("fetch first")))) ||
PushError.Contains(TEXT("cannot lock ref")))
{
// Don't do it during iteration, want to append pull results to InCommand.ResultInfo.ErrorMessages
bWasOutOfDate = true;
break;
}
}
if (bWasOutOfDate)
{
// Get latest
const bool bFetched = GitSourceControlUtils::FetchRemote(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, false,
InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
if (bFetched)
{
// Update local with latest
const bool bPulled = GitSourceControlUtils::PullOrigin(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot,
FGitSourceControlModule::GetEmptyStringArray(), PulledFiles,
InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
if (bPulled)
{
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(
TEXT("push"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, PushParameters,
FGitSourceControlModule::GetEmptyStringArray(), InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
}
}
// Our push still wasn't successful
if (!InCommand.bCommandSuccessful)
{
if (!Provider.bPendingRestart)
{
// If it fails, just let the user do it
FText PushFailMessage(LOCTEXT("GitPush_OutOfDate_Msg", "Git Push failed because there are changes you need to pull.\n\n"
"An attempt was made to pull, but failed, because while the Unreal Editor is "
"open, files cannot always be updated.\n\n"
"Please exit the editor, and update the project again."));
FText PushFailTitle(LOCTEXT("GitPush_OutOfDate_Title", "Git Pull Required"));
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
FMessageDialog::Open(EAppMsgType::Ok, PushFailMessage, PushFailTitle);
#else
FMessageDialog::Open(EAppMsgType::Ok, PushFailMessage, &PushFailTitle);
#endif
UE_LOG(LogSourceControl, Log, TEXT("Push failed because we're out of date, prompting user to resolve manually"));
}
}
}
}
}
else
{
InCommand.bCommandSuccessful = true;
}
// git-lfs: unlock files
if (InCommand.bUsingGitLfsLocking)
{
// If we successfully pushed (or didn't need to push), unlock the files marked for check in
if (InCommand.bCommandSuccessful)
{
// unlock files: execute the LFS command on relative filenames
// (unlock only locked files, that is, not Added files)
TArray<FString> LockedFiles;
GitSourceControlUtils::GetLockedFiles(FilesToCheckIn.Array(), LockedFiles);
if (LockedFiles.Num() > 0)
{
const TArray<FString>& FilesToUnlock = GitSourceControlUtils::RelativeFilenames(LockedFiles, InCommand.PathToGitRoot);
if (FilesToUnlock.Num() > 0)
{
// Not strictly necessary to succeed, so don't update command success
const bool bUnlockSuccess = GitSourceControlUtils::RunLFSCommand(TEXT("unlock"), InCommand.PathToGitRoot, InCommand.PathToGitBinary,
FGitSourceControlModule::GetEmptyStringArray(), FilesToUnlock,
InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
if (bUnlockSuccess)
{
for (const auto& File : LockedFiles)
{
FGitLockedFilesCache::RemoveLockedFile(File);
}
}
}
}
#if 0
for (const FString& File : FilesToCheckIn.Array())
{
FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*File, true);
}
#endif
}
}
// Collect all the files we touched through the pull update
if (bUnpushedFiles && PulledFiles.Num())
{
FilesToCheckIn.Append(PulledFiles);
}
// Before, we added only lockable files from CommittedFiles. But now, we want to update all files, not just lockables.
FilesToCheckIn.Append(CommittedFiles);
// now update the status of our files
TMap<FString, FGitSourceControlState> UpdatedStates;
bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking,
FilesToCheckIn.Array(), InCommand.ResultInfo.ErrorMessages, UpdatedStates);
if (bSuccess)
{
GitSourceControlUtils::CollectNewStates(UpdatedStates, States);
}
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
return InCommand.bCommandSuccessful;
}
InCommand.bCommandSuccessful = false;
return false;
}
bool FGitCheckInWorker::UpdateStates() const
{
return GitSourceControlUtils::UpdateCachedStates(States);
}
FName FGitMarkForAddWorker::GetName() const
{
return "MarkForAdd";
}
bool FGitMarkForAddWorker::Execute(FGitSourceControlCommand& InCommand)
{
// If we have nothing to process, exit immediately
if (InCommand.Files.Num() == 0)
{
return true;
}
check(InCommand.Operation->GetName() == GetName());
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), InCommand.Files, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
if (InCommand.bCommandSuccessful)
{
GitSourceControlUtils::CollectNewStates(InCommand.Files, States, EFileState::Added, ETreeState::Staged);
}
else
{
TMap<FString, FGitSourceControlState> UpdatedStates;
bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates);
if (bSuccess)
{
GitSourceControlUtils::CollectNewStates(UpdatedStates, States);
}
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
}
return InCommand.bCommandSuccessful;
}
bool FGitMarkForAddWorker::UpdateStates() const
{
return GitSourceControlUtils::UpdateCachedStates(States);
}
FName FGitDeleteWorker::GetName() const
{
return "Delete";
}
bool FGitDeleteWorker::Execute(FGitSourceControlCommand& InCommand)
{
// If we have nothing to process, exit immediately
if (InCommand.Files.Num() == 0)
{
return true;
}
check(InCommand.Operation->GetName() == GetName());
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("rm"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), InCommand.Files, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
if (InCommand.bCommandSuccessful)
{
GitSourceControlUtils::CollectNewStates(InCommand.Files, States, EFileState::Deleted, ETreeState::Staged);
}
else
{
TMap<FString, FGitSourceControlState> UpdatedStates;
bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates);
if (bSuccess)
{
GitSourceControlUtils::CollectNewStates(UpdatedStates, States);
}
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
}
return InCommand.bCommandSuccessful;
}
bool FGitDeleteWorker::UpdateStates() const
{
return GitSourceControlUtils::UpdateCachedStates(States);
}
// Get lists of Missing files (ie "deleted"), Modified files, and "other than Added" Existing files
void GetMissingVsExistingFiles(const TArray<FString>& InFiles, TArray<FString>& OutMissingFiles, TArray<FString>& OutAllExistingFiles, TArray<FString>& OutOtherThanAddedExistingFiles)
{
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
const TArray<FString> Files = (InFiles.Num() > 0) ? (InFiles) : (Provider.GetFilesInCache());
TArray<TSharedRef<ISourceControlState, ESPMode::ThreadSafe>> LocalStates;
Provider.GetState(Files, LocalStates, EStateCacheUsage::Use);
for (const auto& State : LocalStates)
{
if (FPaths::FileExists(State->GetFilename()))
{
if (State->IsAdded())
{
OutAllExistingFiles.Add(State->GetFilename());
}
else if (State->IsModified())
{
OutOtherThanAddedExistingFiles.Add(State->GetFilename());
OutAllExistingFiles.Add(State->GetFilename());
}
else if (State->CanRevert()) // for locked but unmodified files
{
OutOtherThanAddedExistingFiles.Add(State->GetFilename());
}
}
else
{
// If already queued for deletion, don't try to delete again
if (State->IsSourceControlled() && !State->IsDeleted())
{
OutMissingFiles.Add(State->GetFilename());
}
}
}
}
FName FGitRevertWorker::GetName() const
{
return "Revert";
}
bool FGitRevertWorker::Execute(FGitSourceControlCommand& InCommand)
{
InCommand.bCommandSuccessful = true;
// Filter files by status
TArray<FString> MissingFiles;
TArray<FString> AllExistingFiles;
TArray<FString> OtherThanAddedExistingFiles;
GetMissingVsExistingFiles(InCommand.Files, MissingFiles, AllExistingFiles, OtherThanAddedExistingFiles);
const bool bRevertAll = InCommand.Files.Num() < 1;
if (bRevertAll)
{
TArray<FString> Parms;
Parms.Add(TEXT("--hard"));
InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("reset"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parms, FGitSourceControlModule::GetEmptyStringArray(), InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
Parms.Reset(2);
Parms.Add(TEXT("-f")); // force
Parms.Add(TEXT("-d")); // remove directories
InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("clean"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parms, FGitSourceControlModule::GetEmptyStringArray(), InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
}
else
{
if (MissingFiles.Num() > 0)
{
// "Added" files that have been deleted needs to be removed from revision control
InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("rm"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), MissingFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
}
if (AllExistingFiles.Num() > 0)
{
// reset and revert any changes already added to the index
InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("reset"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), AllExistingFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("checkout"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), AllExistingFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
}
if (OtherThanAddedExistingFiles.Num() > 0)
{
// revert any changes in working copy (this would fails if the asset was in "Added" state, since after "reset" it is now "untracked")
// may need to try a few times due to file locks from prior operations
bool CheckoutSuccess = false;
int32 Attempts = 10;
while( Attempts-- > 0 )
{
CheckoutSuccess = GitSourceControlUtils::RunCommand(TEXT("checkout"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), OtherThanAddedExistingFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
if (CheckoutSuccess)
{
break;
}
FPlatformProcess::Sleep(0.1f);
}
InCommand.bCommandSuccessful &= CheckoutSuccess;
}
}
if (InCommand.bUsingGitLfsLocking)
{
// unlock files: execute the LFS command on relative filenames
// (unlock only locked files, that is, not Added files)
TArray<FString> LockedFiles;
GitSourceControlUtils::GetLockedFiles(OtherThanAddedExistingFiles, LockedFiles);
if (LockedFiles.Num() > 0)
{
const TArray<FString>& RelativeFiles = GitSourceControlUtils::RelativeFilenames(LockedFiles, InCommand.PathToGitRoot);
InCommand.bCommandSuccessful &= GitSourceControlUtils::RunLFSCommand(TEXT("unlock"), InCommand.PathToGitRoot, InCommand.PathToGitBinary, FGitSourceControlModule::GetEmptyStringArray(), RelativeFiles,
InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
if (InCommand.bCommandSuccessful)
{
for (const auto& File : LockedFiles)
{
FGitLockedFilesCache::RemoveLockedFile(File);
}
}
}
}
// If no files were specified (full revert), refresh all relevant files instead of the specified files (which is an empty list in full revert)
// This is required so that files that were "Marked for add" have their status updated after a full revert.
TArray<FString> FilesToUpdate = InCommand.Files;
if (InCommand.Files.Num() <= 0)
{
for (const auto& File : MissingFiles) FilesToUpdate.Add(File);
for (const auto& File : AllExistingFiles) FilesToUpdate.Add(File);
for (const auto& File : OtherThanAddedExistingFiles) FilesToUpdate.Add(File);
}
// now update the status of our files
TMap<FString, FGitSourceControlState> UpdatedStates;
bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, FilesToUpdate, InCommand.ResultInfo.ErrorMessages, UpdatedStates);
if (bSuccess)
{
GitSourceControlUtils::CollectNewStates(UpdatedStates, States);
}
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
return InCommand.bCommandSuccessful;
}
bool FGitRevertWorker::UpdateStates() const
{
return GitSourceControlUtils::UpdateCachedStates(States);
}
FName FGitSyncWorker::GetName() const
{
return "Sync";
}
bool FGitSyncWorker::Execute(FGitSourceControlCommand& InCommand)
{
TArray<FString> Results;
const bool bFetched = GitSourceControlUtils::FetchRemote(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, false, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
if (!bFetched)
{
return false;
}
InCommand.bCommandSuccessful = GitSourceControlUtils::PullOrigin(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.Files, InCommand.Files, Results, InCommand.ResultInfo.ErrorMessages);
// now update the status of our files
TMap<FString, FGitSourceControlState> UpdatedStates;
const bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking,
InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates);
if (bSuccess)
{
GitSourceControlUtils::CollectNewStates(UpdatedStates, States);
}
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary);
return InCommand.bCommandSuccessful;
}
bool FGitSyncWorker::UpdateStates() const
{
return GitSourceControlUtils::UpdateCachedStates(States);
}
FName FGitFetch::GetName() const
{
return "Fetch";
}
FText FGitFetch::GetInProgressString() const
{
// TODO Configure origin
return LOCTEXT("SourceControl_Push", "Fetching from remote origin...");
}
FName FGitFetchWorker::GetName() const
{
return "Fetch";
}
bool FGitFetchWorker::Execute(FGitSourceControlCommand& InCommand)
{
InCommand.bCommandSuccessful = GitSourceControlUtils::FetchRemote(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking,
InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
if (!InCommand.bCommandSuccessful)
{
return false;
}
check(InCommand.Operation->GetName() == GetName());
TSharedRef<FGitFetch, ESPMode::ThreadSafe> Operation = StaticCastSharedRef<FGitFetch>(InCommand.Operation);
if (Operation->bUpdateStatus)
{
// Now update the status of all our files
const TArray<FString> ProjectDirs {FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()),FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir()),
FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath())};
TMap<FString, FGitSourceControlState> UpdatedStates;
InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking,
ProjectDirs, InCommand.ResultInfo.ErrorMessages, UpdatedStates);
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
if (InCommand.bCommandSuccessful)
{
GitSourceControlUtils::CollectNewStates(UpdatedStates, States);
}
}
return InCommand.bCommandSuccessful;
}
bool FGitFetchWorker::UpdateStates() const
{
return GitSourceControlUtils::UpdateCachedStates(States);
}
FName FGitUpdateStatusWorker::GetName() const
{
return "UpdateStatus";
}
bool FGitUpdateStatusWorker::Execute(FGitSourceControlCommand& InCommand)
{
check(InCommand.Operation->GetName() == GetName());
TSharedRef<FUpdateStatus, ESPMode::ThreadSafe> Operation = StaticCastSharedRef<FUpdateStatus>(InCommand.Operation);
if(InCommand.Files.Num() > 0)
{
TMap<FString, FGitSourceControlState> UpdatedStates;
InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates);
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
if (InCommand.bCommandSuccessful)
{
GitSourceControlUtils::CollectNewStates(UpdatedStates, States);
if (Operation->ShouldUpdateHistory())
{
for (const auto& State : UpdatedStates)
{
const FString& File = State.Key;
TGitSourceControlHistory History;
if (State.Value.IsConflicted())
{
// In case of a merge conflict, we first need to get the tip of the "remote branch" (MERGE_HEAD)
GitSourceControlUtils::RunGetHistory(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, File, true,
InCommand.ResultInfo.ErrorMessages, History);
}
// Get the history of the file in the current branch
InCommand.bCommandSuccessful &= GitSourceControlUtils::RunGetHistory(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, File, false,
InCommand.ResultInfo.ErrorMessages, History);
Histories.Add(*File, History);
}
}
}
}
else
{
// no path provided: only update the status of assets in Content/ directory and also Config files
const TArray<FString> ProjectDirs {FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()), FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir()),
FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath())};
TMap<FString, FGitSourceControlState> UpdatedStates;
InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, ProjectDirs, InCommand.ResultInfo.ErrorMessages, UpdatedStates);
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
if (InCommand.bCommandSuccessful)
{
GitSourceControlUtils::CollectNewStates(UpdatedStates, States);
}
}
GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary);
// don't use the ShouldUpdateModifiedState() hint here as it is specific to Perforce: the above normal Git status has already told us this information (like Git and Mercurial)
return InCommand.bCommandSuccessful;
}
bool FGitUpdateStatusWorker::UpdateStates() const
{
bool bUpdated = GitSourceControlUtils::UpdateCachedStates(States);
FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>( "GitSourceControl" );
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
const bool bUsingGitLfsLocking = Provider.UsesCheckout();
// TODO without LFS : Workaround a bug with the Source Control Module not updating file state after a simple "Save" with no "Checkout" (when not using File Lock)
const FDateTime Now = bUsingGitLfsLocking ? FDateTime::Now() : FDateTime::MinValue();
// add history, if any
for(const auto& History : Histories)
{
TSharedRef<FGitSourceControlState, ESPMode::ThreadSafe> State = Provider.GetStateInternal(History.Key);
State->History = History.Value;
State->TimeStamp = Now;
bUpdated = true;
}
return bUpdated;
}
FName FGitCopyWorker::GetName() const
{
return "Copy";
}
bool FGitCopyWorker::Execute(FGitSourceControlCommand& InCommand)
{
check(InCommand.Operation->GetName() == GetName());
// Copy or Move operation on a single file : Git does not need an explicit copy nor move,
// but after a Move the Editor create a redirector file with the old asset name that points to the new asset.
// The redirector needs to be committed with the new asset to perform a real rename.
// => the following is to "MarkForAdd" the redirector, but it still need to be committed by selecting the whole directory and "check-in"
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), InCommand.Files, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
if (InCommand.bCommandSuccessful)
{
GitSourceControlUtils::CollectNewStates(InCommand.Files, States, EFileState::Added, ETreeState::Staged);
}
else
{
TMap<FString, FGitSourceControlState> UpdatedStates;
const bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates);
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
if (bSuccess)
{
GitSourceControlUtils::CollectNewStates(UpdatedStates, States);
}
}
return InCommand.bCommandSuccessful;
}
bool FGitCopyWorker::UpdateStates() const
{
return GitSourceControlUtils::UpdateCachedStates(States);
}
FName FGitResolveWorker::GetName() const
{
return "Resolve";
}
bool FGitResolveWorker::Execute( class FGitSourceControlCommand& InCommand )
{
check(InCommand.Operation->GetName() == GetName());
// mark the conflicting files as resolved:
TArray<FString> Results;
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), InCommand.Files, Results, InCommand.ResultInfo.ErrorMessages);
// now update the status of our files
TMap<FString, FGitSourceControlState> UpdatedStates;
const bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates);
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
if (bSuccess)
{
GitSourceControlUtils::CollectNewStates(UpdatedStates, States);
}
return InCommand.bCommandSuccessful;
}
bool FGitResolveWorker::UpdateStates() const
{
return GitSourceControlUtils::UpdateCachedStates(States);
}
FName FGitMoveToChangelistWorker::GetName() const
{
return "MoveToChangelist";
}
bool FGitMoveToChangelistWorker::UpdateStates() const
{
return true;
}
bool FGitMoveToChangelistWorker::Execute(FGitSourceControlCommand& InCommand)
{
check(InCommand.Operation->GetName() == GetName());
FGitSourceControlChangelist DestChangelist = InCommand.Changelist;
bool bResult = false;
if(DestChangelist.GetName().Equals(TEXT("Staged")))
{
bResult = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), InCommand.Files, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
}
else if(DestChangelist.GetName().Equals(TEXT("Working")))
{
TArray<FString> Parameter;
Parameter.Add(TEXT("--staged"));
bResult = GitSourceControlUtils::RunCommand(TEXT("restore"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameter, InCommand.Files, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
}
if (bResult)
{
TMap<FString, FGitSourceControlState> DummyStates;
GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.InfoMessages, DummyStates);
}
return bResult;
}
FName FGitUpdateStagingWorker::GetName() const
{
return "UpdateChangelistsStatus";
}
bool FGitUpdateStagingWorker::Execute(FGitSourceControlCommand& InCommand)
{
return GitSourceControlUtils::UpdateChangelistStateByCommand();
}
bool FGitUpdateStagingWorker::UpdateStates() const
{
return true;
}
#undef LOCTEXT_NAMESPACE

View File

@@ -0,0 +1,210 @@
// 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)
#pragma once
#include "CoreMinimal.h"
#include "IGitSourceControlWorker.h"
#include "GitSourceControlState.h"
#include "ISourceControlOperation.h"
/**
* Internal operation used to fetch from remote
*/
class FGitFetch : public ISourceControlOperation
{
public:
// ISourceControlOperation interface
virtual FName GetName() const override;
virtual FText GetInProgressString() const override;
bool bUpdateStatus = false;
};
/** Called when first activated on a project, and then at project load time.
* Look for the root directory of the git repository (where the ".git/" subdirectory is located). */
class FGitConnectWorker : public IGitSourceControlWorker
{
public:
virtual ~FGitConnectWorker() {}
// IGitSourceControlWorker interface
virtual FName GetName() const override;
virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
virtual bool UpdateStates() const override;
/** Temporary states for results */
TMap<const FString, FGitState> States;
};
/** Lock (check-out) a set of files using Git LFS 2. */
class FGitCheckOutWorker : public IGitSourceControlWorker
{
public:
virtual ~FGitCheckOutWorker() {}
// IGitSourceControlWorker interface
virtual FName GetName() const override;
virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
virtual bool UpdateStates() const override;
/** Temporary states for results */
TMap<const FString, FGitState> States;
};
/** Commit (check-in) a set of files to the local depot. */
class FGitCheckInWorker : public IGitSourceControlWorker
{
public:
virtual ~FGitCheckInWorker() {}
// IGitSourceControlWorker interface
virtual FName GetName() const override;
virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
virtual bool UpdateStates() const override;
/** Temporary states for results */
TMap<const FString, FGitState> States;
};
/** Add an untracked file to revision control (so only a subset of the git add command). */
class FGitMarkForAddWorker : public IGitSourceControlWorker
{
public:
virtual ~FGitMarkForAddWorker() {}
// IGitSourceControlWorker interface
virtual FName GetName() const override;
virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
virtual bool UpdateStates() const override;
/** Temporary states for results */
TMap<const FString, FGitState> States;
};
/** Delete a file and remove it from revision control. */
class FGitDeleteWorker : public IGitSourceControlWorker
{
public:
virtual ~FGitDeleteWorker() {}
// IGitSourceControlWorker interface
virtual FName GetName() const override;
virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
virtual bool UpdateStates() const override;
/** Temporary states for results */
TMap<const FString, FGitState> States;
};
/** Revert any change to a file to its state on the local depot. */
class FGitRevertWorker : public IGitSourceControlWorker
{
public:
virtual ~FGitRevertWorker() {}
// IGitSourceControlWorker interface
virtual FName GetName() const override;
virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
virtual bool UpdateStates() const override;
/** Temporary states for results */
TMap<const FString, FGitState> States;
};
/** Git pull --rebase to update branch from its configured remote */
class FGitSyncWorker : public IGitSourceControlWorker
{
public:
virtual ~FGitSyncWorker() {}
// IGitSourceControlWorker interface
virtual FName GetName() const override;
virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
virtual bool UpdateStates() const override;
/** Temporary states for results */
TMap<const FString, FGitState> States;
};
/** Get revision control status of files on local working copy. */
class FGitUpdateStatusWorker : public IGitSourceControlWorker
{
public:
virtual ~FGitUpdateStatusWorker() {}
// IGitSourceControlWorker interface
virtual FName GetName() const override;
virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
virtual bool UpdateStates() const override;
public:
/** Temporary states for results */
TMap<const FString, FGitState> States;
/** Map of filenames to history */
TMap<FString, TGitSourceControlHistory> Histories;
};
/** Copy or Move operation on a single file */
class FGitCopyWorker : public IGitSourceControlWorker
{
public:
virtual ~FGitCopyWorker() {}
// IGitSourceControlWorker interface
virtual FName GetName() const override;
virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
virtual bool UpdateStates() const override;
/** Temporary states for results */
TMap<const FString, FGitState> States;
};
/** git add to mark a conflict as resolved */
class FGitResolveWorker : public IGitSourceControlWorker
{
public:
virtual ~FGitResolveWorker() {}
virtual FName GetName() const override;
virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
virtual bool UpdateStates() const override;
/** Temporary states for results */
TMap<const FString, FGitState> States;
};
/** Git push to publish branch for its configured remote */
class FGitFetchWorker : public IGitSourceControlWorker
{
public:
virtual ~FGitFetchWorker() {}
// IGitSourceControlWorker interface
virtual FName GetName() const override;
virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
virtual bool UpdateStates() const override;
/** Temporary states for results */
TMap<const FString, FGitState> States;
};
class FGitMoveToChangelistWorker : public IGitSourceControlWorker
{
public:
virtual ~FGitMoveToChangelistWorker() {}
// IGitSourceControlWorker interface
virtual FName GetName() const override;
virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
virtual bool UpdateStates() const override;
/** Temporary states for results */
TMap<const FString, FGitState> States;
};
class FGitUpdateStagingWorker: public IGitSourceControlWorker
{
public:
virtual ~FGitUpdateStagingWorker() {}
// IGitSourceControlWorker interface
virtual FName GetName() const override;
virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
virtual bool UpdateStates() const override;
/** Temporary states for results */
TMap<const FString, FGitState> States;
};

View File

@@ -0,0 +1,909 @@
// Copyright (c) 2014-2023 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 "GitSourceControlProvider.h"
#include "GitMessageLog.h"
#include "GitSourceControlState.h"
#include "Misc/Paths.h"
#include "Misc/QueuedThreadPool.h"
#include "GitSourceControlCommand.h"
#include "ISourceControlModule.h"
#include "GitSourceControlModule.h"
#include "GitSourceControlUtils.h"
#include "SGitSourceControlSettings.h"
#include "GitSourceControlRunner.h"
#include "GitSourceControlChangelistState.h"
#include "Logging/MessageLog.h"
#include "ScopedSourceControlProgress.h"
#include "SourceControlHelpers.h"
#include "SourceControlOperations.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Async/Async.h"
#include "GenericPlatform/GenericPlatformFile.h"
#include "HAL/FileManager.h"
#include "Interfaces/IPluginManager.h"
#include "Misc/App.h"
#include "Misc/EngineVersion.h"
#include "Misc/MessageDialog.h"
#include "UObject/ObjectSaveContext.h"
#define LOCTEXT_NAMESPACE "GitSourceControl"
static FName ProviderName("Git LFS 2");
void FGitSourceControlProvider::Init(bool bForceConnection)
{
// Init() is called multiple times at startup: do not check git each time
if(!bGitAvailable)
{
const TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("GitSourceControl"));
if(Plugin.IsValid())
{
UE_LOG(LogSourceControl, Log, TEXT("Git plugin '%s'"), *(Plugin->GetDescriptor().VersionName));
}
CheckGitAvailability();
}
UPackage::PackageSavedWithContextEvent.AddStatic(&GitSourceControlUtils::UpdateFileStagingOnSaved);
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
AssetRegistryModule.Get().OnAssetRenamed().AddStatic(&GitSourceControlUtils::UpdateStateOnAssetRename);
// bForceConnection: not used anymore
}
void FGitSourceControlProvider::CheckGitAvailability()
{
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
if(PathToGitBinary.IsEmpty())
{
// Try to find Git binary, and update settings accordingly
PathToGitBinary = GitSourceControlUtils::FindGitBinaryPath();
if(!PathToGitBinary.IsEmpty())
{
GitSourceControl.AccessSettings().SetBinaryPath(PathToGitBinary);
}
}
if(!PathToGitBinary.IsEmpty())
{
UE_LOG(LogSourceControl, Log, TEXT("Using '%s'"), *PathToGitBinary);
bGitAvailable = true;
CheckRepositoryStatus();
}
else
{
bGitAvailable = false;
}
}
void FGitSourceControlProvider::UpdateSettings()
{
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
bUsingGitLfsLocking = GitSourceControl.AccessSettings().IsUsingGitLfsLocking();
LockUser = GitSourceControl.AccessSettings().GetLfsUserName();
}
void FGitSourceControlProvider::CheckRepositoryStatus()
{
GitSourceControlMenu.Register();
// Make sure our settings our up to date
UpdateSettings();
// Find the path to the root Git directory (if any, else uses the ProjectDir)
const FString PathToProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
PathToRepositoryRoot = PathToProjectDir;
if (!GitSourceControlUtils::FindRootDirectory(PathToProjectDir, PathToGitRoot))
{
UE_LOG(LogSourceControl, Error, TEXT("Failed to find valid Git root directory."));
bGitRepositoryFound = false;
return;
}
PathToRepositoryRoot = PathToGitRoot;
if (!GitSourceControlUtils::CheckGitAvailability(PathToGitBinary, &GitVersion))
{
UE_LOG(LogSourceControl, Error, TEXT("Failed to find valid Git executable."));
bGitRepositoryFound = false;
return;
}
TUniqueFunction<void()> InitFunc = [this]()
{
if (!IsInGameThread())
{
// Wait until the module interface is valid
IModuleInterface* GitModule;
do
{
GitModule = FModuleManager::Get().GetModule("GitSourceControl");
FPlatformProcess::Sleep(0.0f);
} while (!GitModule);
}
// Get user name & email (of the repository, else from the global Git config)
GitSourceControlUtils::GetUserConfig(PathToGitBinary, PathToRepositoryRoot, UserName, UserEmail);
TMap<FString, FGitSourceControlState> States;
auto ConditionalRepoInit = [this, &States]()
{
if (!GitSourceControlUtils::GetBranchName(PathToGitBinary, PathToRepositoryRoot, BranchName))
{
return false;
}
GitSourceControlUtils::GetRemoteBranchName(PathToGitBinary, PathToRepositoryRoot, RemoteBranchName);
GitSourceControlUtils::GetRemoteUrl(PathToGitBinary, PathToRepositoryRoot, RemoteUrl);
const TArray<FString> Files{TEXT("*.uasset"), TEXT("*.umap")};
TArray<FString> LockableErrorMessages;
if (!GitSourceControlUtils::CheckLFSLockable(PathToGitBinary, PathToRepositoryRoot, Files, LockableErrorMessages))
{
for (const auto &ErrorMessage : LockableErrorMessages)
{
UE_LOG(LogSourceControl, Error, TEXT("%s"), *ErrorMessage);
}
}
const TArray<FString> ProjectDirs{FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()),
FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir()),
FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath())};
TArray<FString> StatusErrorMessages;
if (!GitSourceControlUtils::RunUpdateStatus(PathToGitBinary, PathToRepositoryRoot, bUsingGitLfsLocking, ProjectDirs, StatusErrorMessages, States))
{
return false;
}
return true;
};
if (ConditionalRepoInit())
{
TUniqueFunction<void()> SuccessFunc = [States, this]()
{
TMap<const FString, FGitState> Results;
if (GitSourceControlUtils::CollectNewStates(States, Results))
{
GitSourceControlUtils::UpdateCachedStates(Results);
}
Runner = new FGitSourceControlRunner();
bGitRepositoryFound = true;
};
if (FApp::IsUnattended() || IsRunningCommandlet())
{
SuccessFunc();
}
else
{
AsyncTask(ENamedThreads::GameThread, MoveTemp(SuccessFunc));
}
}
else
{
TUniqueFunction<void()> ErrorFunc = [States, this]()
{
UE_LOG(LogSourceControl, Error, TEXT("Failed to update repo on initialization."));
bGitRepositoryFound = false;
};
if (FApp::IsUnattended() || IsRunningCommandlet())
{
ErrorFunc();
}
else
{
AsyncTask(ENamedThreads::GameThread, MoveTemp(ErrorFunc));
}
}
};
if (FApp::IsUnattended() || IsRunningCommandlet())
{
InitFunc();
}
else
{
AsyncTask(ENamedThreads::AnyHiPriThreadNormalTask, MoveTemp(InitFunc));
}
}
void FGitSourceControlProvider::SetLastErrors(const TArray<FText>& InErrors)
{
FScopeLock Lock(&LastErrorsCriticalSection);
LastErrors = InErrors;
}
TArray<FText> FGitSourceControlProvider::GetLastErrors() const
{
FScopeLock Lock(&LastErrorsCriticalSection);
TArray<FText> Result = LastErrors;
return Result;
}
int32 FGitSourceControlProvider::GetNumLastErrors() const
{
FScopeLock Lock(&LastErrorsCriticalSection);
return LastErrors.Num();
}
void FGitSourceControlProvider::Close()
{
// clear the cache
StateCache.Empty();
// Remove all extensions to the "Revision Control" menu in the Editor Toolbar
GitSourceControlMenu.Unregister();
bGitAvailable = false;
bGitRepositoryFound = false;
UserName.Empty();
UserEmail.Empty();
if (Runner)
{
delete Runner;
Runner = nullptr;
}
}
TSharedRef<FGitSourceControlState, ESPMode::ThreadSafe> FGitSourceControlProvider::GetStateInternal(const FString& Filename)
{
TSharedRef<FGitSourceControlState, ESPMode::ThreadSafe>* State = StateCache.Find(Filename);
if (State != NULL)
{
// found cached item
return (*State);
}
else
{
// cache an unknown state for this item
TSharedRef<FGitSourceControlState, ESPMode::ThreadSafe> NewState = MakeShareable( new FGitSourceControlState(Filename) );
StateCache.Add(Filename, NewState);
return NewState;
}
}
TSharedRef<FGitSourceControlChangelistState, ESPMode::ThreadSafe> FGitSourceControlProvider::GetStateInternal(const FGitSourceControlChangelist& InChangelist)
{
TSharedRef<FGitSourceControlChangelistState, ESPMode::ThreadSafe>* State = ChangelistsStateCache.Find(InChangelist);
if (State != NULL)
{
// found cached item
return (*State);
}
else
{
// cache an unknown state for this item
TSharedRef<FGitSourceControlChangelistState, ESPMode::ThreadSafe> NewState = MakeShared<FGitSourceControlChangelistState>(InChangelist);
ChangelistsStateCache.Add(InChangelist, NewState);
return NewState;
}
}
FText FGitSourceControlProvider::GetStatusText() const
{
FFormatNamedArguments Args;
Args.Add(TEXT("IsAvailable"), (IsEnabled() && IsAvailable()) ? LOCTEXT("Yes", "Yes") : LOCTEXT("No", "No"));
Args.Add( TEXT("RepositoryName"), FText::FromString(PathToRepositoryRoot) );
Args.Add( TEXT("RemoteUrl"), FText::FromString(RemoteUrl) );
Args.Add( TEXT("UserName"), FText::FromString(UserName) );
Args.Add( TEXT("UserEmail"), FText::FromString(UserEmail) );
Args.Add( TEXT("BranchName"), FText::FromString(BranchName) );
Args.Add( TEXT("CommitId"), FText::FromString(CommitId.Left(8)) );
Args.Add( TEXT("CommitSummary"), FText::FromString(CommitSummary) );
FText FormattedError;
const TArray<FText>& RecentErrors = GetLastErrors();
if (RecentErrors.Num() > 0)
{
FFormatNamedArguments ErrorArgs;
ErrorArgs.Add(TEXT("ErrorText"), RecentErrors[0]);
FormattedError = FText::Format(LOCTEXT("GitErrorStatusText", "Error: {ErrorText}\n\n"), ErrorArgs);
}
Args.Add(TEXT("ErrorText"), FormattedError);
return FText::Format( NSLOCTEXT("GitStatusText", "{ErrorText}Enabled: {IsAvailable}", "Local repository: {RepositoryName}\nRemote: {RemoteUrl}\nUser: {UserName}\nE-mail: {UserEmail}\n[{BranchName} {CommitId}] {CommitSummary}"), Args );
}
/** Quick check if revision control is enabled */
bool FGitSourceControlProvider::IsEnabled() const
{
return bGitRepositoryFound;
}
/** Quick check if revision control is available for use (useful for server-based providers) */
bool FGitSourceControlProvider::IsAvailable() const
{
return bGitRepositoryFound;
}
const FName& FGitSourceControlProvider::GetName(void) const
{
return ProviderName;
}
ECommandResult::Type FGitSourceControlProvider::GetState( const TArray<FString>& InFiles, TArray< TSharedRef<ISourceControlState, ESPMode::ThreadSafe> >& OutState, EStateCacheUsage::Type InStateCacheUsage )
{
if (!IsEnabled())
{
return ECommandResult::Failed;
}
if (InStateCacheUsage == EStateCacheUsage::ForceUpdate)
{
TArray<FString> ForceUpdate;
for (FString Path : InFiles)
{
// Remove the path from the cache, so it's not ignored the next time we force check.
// If the file isn't in the cache, force update it now.
if (!RemoveFileFromIgnoreForceCache(Path))
{
ForceUpdate.Add(Path);
}
}
if (ForceUpdate.Num() > 0)
{
Execute(ISourceControlOperation::Create<FUpdateStatus>(), ForceUpdate);
}
}
const TArray<FString>& AbsoluteFiles = SourceControlHelpers::AbsoluteFilenames(InFiles);
for (TArray<FString>::TConstIterator It(AbsoluteFiles); It; It++)
{
OutState.Add(GetStateInternal(*It));
}
return ECommandResult::Succeeded;
}
#if ENGINE_MAJOR_VERSION >= 5
ECommandResult::Type FGitSourceControlProvider::GetState(const TArray<FSourceControlChangelistRef>& InChangelists, TArray<FSourceControlChangelistStateRef>& OutState, EStateCacheUsage::Type InStateCacheUsage)
{
if (!IsEnabled())
{
return ECommandResult::Failed;
}
for (FSourceControlChangelistRef Changelist : InChangelists)
{
FGitSourceControlChangelistRef GitChangelist = StaticCastSharedRef<FGitSourceControlChangelist>(Changelist);
OutState.Add(GetStateInternal(GitChangelist.Get()));
}
return ECommandResult::Succeeded;
}
#endif
TArray<FSourceControlStateRef> FGitSourceControlProvider::GetCachedStateByPredicate(TFunctionRef<bool(const FSourceControlStateRef&)> Predicate) const
{
TArray<FSourceControlStateRef> Result;
for (const auto& CacheItem : StateCache)
{
const FSourceControlStateRef& State = CacheItem.Value;
if (Predicate(State))
{
Result.Add(State);
}
}
return Result;
}
bool FGitSourceControlProvider::RemoveFileFromCache(const FString& Filename)
{
return StateCache.Remove(Filename) > 0;
}
bool FGitSourceControlProvider::AddFileToIgnoreForceCache(const FString& Filename)
{
return IgnoreForceCache.Add(Filename) > 0;
}
bool FGitSourceControlProvider::RemoveFileFromIgnoreForceCache(const FString& Filename)
{
return IgnoreForceCache.Remove(Filename) > 0;
}
/** Get files in cache */
TArray<FString> FGitSourceControlProvider::GetFilesInCache()
{
TArray<FString> Files;
for (const auto& State : StateCache)
{
Files.Add(State.Key);
}
return Files;
}
FDelegateHandle FGitSourceControlProvider::RegisterSourceControlStateChanged_Handle( const FSourceControlStateChanged::FDelegate& SourceControlStateChanged )
{
return OnSourceControlStateChanged.Add( SourceControlStateChanged );
}
void FGitSourceControlProvider::UnregisterSourceControlStateChanged_Handle( FDelegateHandle Handle )
{
OnSourceControlStateChanged.Remove( Handle );
}
#if ENGINE_MAJOR_VERSION < 5
ECommandResult::Type FGitSourceControlProvider::Execute( const FSourceControlOperationRef& InOperation, const TArray<FString>& InFiles, EConcurrency::Type InConcurrency, const FSourceControlOperationComplete& InOperationCompleteDelegate )
#else
ECommandResult::Type FGitSourceControlProvider::Execute( const FSourceControlOperationRef& InOperation, FSourceControlChangelistPtr InChangelist, const TArray<FString>& InFiles, EConcurrency::Type InConcurrency, const FSourceControlOperationComplete& InOperationCompleteDelegate )
#endif
{
if(!IsEnabled() && !(InOperation->GetName() == "Connect")) // Only Connect operation allowed while not Enabled (Repository found)
{
InOperationCompleteDelegate.ExecuteIfBound(InOperation, ECommandResult::Failed);
return ECommandResult::Failed;
}
const TArray<FString>& AbsoluteFiles = SourceControlHelpers::AbsoluteFilenames(InFiles);
// Query to see if we allow this operation
TSharedPtr<IGitSourceControlWorker, ESPMode::ThreadSafe> Worker = CreateWorker(InOperation->GetName());
if(!Worker.IsValid())
{
// this operation is unsupported by this revision control provider
FFormatNamedArguments Arguments;
Arguments.Add( TEXT("OperationName"), FText::FromName(InOperation->GetName()) );
Arguments.Add( TEXT("ProviderName"), FText::FromName(GetName()) );
FText Message(FText::Format(LOCTEXT("UnsupportedOperation", "Operation '{OperationName}' not supported by revision control provider '{ProviderName}'"), Arguments));
FTSMessageLog("SourceControl").Error(Message);
InOperation->AddErrorMessge(Message);
InOperationCompleteDelegate.ExecuteIfBound(InOperation, ECommandResult::Failed);
return ECommandResult::Failed;
}
FGitSourceControlCommand* Command = new FGitSourceControlCommand(InOperation, Worker.ToSharedRef());
Command->Files = AbsoluteFiles;
Command->UpdateRepositoryRootIfSubmodule(AbsoluteFiles);
Command->OperationCompleteDelegate = InOperationCompleteDelegate;
TSharedPtr<FGitSourceControlChangelist, ESPMode::ThreadSafe> ChangelistPtr = StaticCastSharedPtr<FGitSourceControlChangelist>(InChangelist);
Command->Changelist = ChangelistPtr ? ChangelistPtr.ToSharedRef().Get() : FGitSourceControlChangelist();
// fire off operation
if(InConcurrency == EConcurrency::Synchronous)
{
Command->bAutoDelete = false;
#if UE_BUILD_DEBUG
UE_LOG(LogSourceControl, Log, TEXT("ExecuteSynchronousCommand(%s)"), *InOperation->GetName().ToString());
#endif
return ExecuteSynchronousCommand(*Command, InOperation->GetInProgressString(), false);
}
else
{
Command->bAutoDelete = true;
#if UE_BUILD_DEBUG
UE_LOG(LogSourceControl, Log, TEXT("IssueAsynchronousCommand(%s)"), *InOperation->GetName().ToString());
#endif
return IssueCommand(*Command);
}
}
#if ENGINE_MAJOR_VERSION < 5
bool FGitSourceControlProvider::CanCancelOperation( const FSourceControlOperationRef& InOperation ) const
#else
bool FGitSourceControlProvider::CanCancelOperation( const FSourceControlOperationRef& InOperation ) const
#endif
{
// TODO: maybe support cancellation again?
#if 0
for (int32 CommandIndex = 0; CommandIndex < CommandQueue.Num(); ++CommandIndex)
{
const FGitSourceControlCommand& Command = *CommandQueue[CommandIndex];
if (Command.Operation == InOperation)
{
check(Command.bAutoDelete);
return true;
}
}
#endif
// operation was not in progress!
return false;
}
#if ENGINE_MAJOR_VERSION < 5
void FGitSourceControlProvider::CancelOperation( const FSourceControlOperationRef& InOperation )
#else
void FGitSourceControlProvider::CancelOperation( const FSourceControlOperationRef& InOperation )
#endif
{
for (int32 CommandIndex = 0; CommandIndex < CommandQueue.Num(); ++CommandIndex)
{
FGitSourceControlCommand& Command = *CommandQueue[CommandIndex];
if (Command.Operation == InOperation)
{
check(Command.bAutoDelete);
Command.Cancel();
return;
}
}
}
bool FGitSourceControlProvider::UsesLocalReadOnlyState() const
{
return bUsingGitLfsLocking; // Git LFS Lock uses read-only state
}
bool FGitSourceControlProvider::UsesChangelists() const
{
return true;
}
bool FGitSourceControlProvider::UsesCheckout() const
{
return bUsingGitLfsLocking; // Git LFS Lock uses read-only state
}
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
bool FGitSourceControlProvider::UsesFileRevisions() const
{
return true;
}
TOptional<bool> FGitSourceControlProvider::IsAtLatestRevision() const
{
return TOptional<bool>();
}
TOptional<int> FGitSourceControlProvider::GetNumLocalChanges() const
{
return TOptional<int>();
}
#endif
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2
bool FGitSourceControlProvider::AllowsDiffAgainstDepot() const
{
return true;
}
bool FGitSourceControlProvider::UsesUncontrolledChangelists() const
{
return true;
}
bool FGitSourceControlProvider::UsesSnapshots() const
{
return false;
}
#endif
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
bool FGitSourceControlProvider::CanExecuteOperation(const FSourceControlOperationRef& InOperation) const {
return WorkersMap.Find(InOperation->GetName()) != nullptr;
}
TMap<ISourceControlProvider::EStatus, FString> FGitSourceControlProvider::GetStatus() const
{
TMap<EStatus, FString> Result;
Result.Add(EStatus::Enabled, IsEnabled() ? TEXT("Yes") : TEXT("No") );
Result.Add(EStatus::Connected, (IsEnabled() && IsAvailable()) ? TEXT("Yes") : TEXT("No") );
Result.Add(EStatus::User, UserName);
Result.Add(EStatus::Repository, PathToRepositoryRoot);
Result.Add(EStatus::Remote, RemoteUrl);
Result.Add(EStatus::Branch, BranchName);
Result.Add(EStatus::Email, UserEmail);
return Result;
}
#endif
TSharedPtr<IGitSourceControlWorker, ESPMode::ThreadSafe> FGitSourceControlProvider::CreateWorker(const FName& InOperationName) const
{
const FGetGitSourceControlWorker* Operation = WorkersMap.Find(InOperationName);
if(Operation != nullptr)
{
return Operation->Execute();
}
return nullptr;
}
void FGitSourceControlProvider::RegisterWorker( const FName& InName, const FGetGitSourceControlWorker& InDelegate )
{
WorkersMap.Add( InName, InDelegate );
}
void FGitSourceControlProvider::OutputCommandMessages(const FGitSourceControlCommand& InCommand) const
{
FTSMessageLog SourceControlLog("SourceControl");
for (int32 ErrorIndex = 0; ErrorIndex < InCommand.ResultInfo.ErrorMessages.Num(); ++ErrorIndex)
{
SourceControlLog.Error(FText::FromString(InCommand.ResultInfo.ErrorMessages[ErrorIndex]));
}
for (int32 InfoIndex = 0; InfoIndex < InCommand.ResultInfo.InfoMessages.Num(); ++InfoIndex)
{
SourceControlLog.Info(FText::FromString(InCommand.ResultInfo.InfoMessages[InfoIndex]));
}
}
void FGitSourceControlProvider::UpdateRepositoryStatus(const class FGitSourceControlCommand& InCommand)
{
// For all operations running UpdateStatus, get Commit information:
if (!InCommand.CommitId.IsEmpty())
{
CommitId = InCommand.CommitId;
CommitSummary = InCommand.CommitSummary;
}
}
void FGitSourceControlProvider::Tick()
{
#if ENGINE_MAJOR_VERSION < 5
bool bStatesUpdated = false;
#else
bool bStatesUpdated = TicksUntilNextForcedUpdate == 1;
if( TicksUntilNextForcedUpdate > 0 )
{
--TicksUntilNextForcedUpdate;
}
#endif
for (int32 CommandIndex = 0; CommandIndex < CommandQueue.Num(); ++CommandIndex)
{
FGitSourceControlCommand& Command = *CommandQueue[CommandIndex];
if (Command.bExecuteProcessed)
{
// Remove command from the queue
CommandQueue.RemoveAt(CommandIndex);
if (!Command.IsCanceled())
{
// Update repository status on UpdateStatus operations
UpdateRepositoryStatus(Command);
}
// let command update the states of any files
bStatesUpdated |= Command.Worker->UpdateStates();
// dump any messages to output log
OutputCommandMessages(Command);
// run the completion delegate callback if we have one bound
if (!Command.IsCanceled())
{
Command.ReturnResults();
}
// commands that are left in the array during a tick need to be deleted
if(Command.bAutoDelete)
{
// Only delete commands that are not running 'synchronously'
delete &Command;
}
// only do one command per tick loop, as we dont want concurrent modification
// of the command queue (which can happen in the completion delegate)
break;
}
else if (Command.bCancelled)
{
// If this was a synchronous command, set it free so that it will be deleted automatically
// when its (still running) thread finally finishes
Command.bAutoDelete = true;
Command.ReturnResults();
break;
}
}
if (bStatesUpdated)
{
OnSourceControlStateChanged.Broadcast();
}
}
TArray< TSharedRef<ISourceControlLabel> > FGitSourceControlProvider::GetLabels( const FString& InMatchingSpec ) const
{
TArray< TSharedRef<ISourceControlLabel> > Tags;
// NOTE list labels. Called by CrashDebugHelper() (to remote debug Engine crash)
// and by SourceControlHelpers::AnnotateFile() (to add source file to report)
// Reserved for internal use by Epic Games with Perforce only
return Tags;
}
#if ENGINE_MAJOR_VERSION >= 5
TArray<FSourceControlChangelistRef> FGitSourceControlProvider::GetChangelists( EStateCacheUsage::Type InStateCacheUsage )
{
if (!IsEnabled())
{
return TArray<FSourceControlChangelistRef>();
}
TArray<FSourceControlChangelistRef> Changelists;
Algo::Transform(ChangelistsStateCache, Changelists, [](const auto& Pair) { return MakeShared<FGitSourceControlChangelist, ESPMode::ThreadSafe>(Pair.Key); });
return Changelists;
}
#endif
#if SOURCE_CONTROL_WITH_SLATE
TSharedRef<class SWidget> FGitSourceControlProvider::MakeSettingsWidget() const
{
return SNew(SGitSourceControlSettings);
}
#endif
ECommandResult::Type FGitSourceControlProvider::ExecuteSynchronousCommand(FGitSourceControlCommand& InCommand, const FText& Task, bool bSuppressResponseMsg)
{
ECommandResult::Type Result = ECommandResult::Failed;
struct Local
{
static void CancelCommand(FGitSourceControlCommand* InControlCommand)
{
InControlCommand->Cancel();
}
};
FText TaskText = Task;
// Display the progress dialog
if (bSuppressResponseMsg)
{
TaskText = FText::GetEmpty();
}
int i = 0;
// Display the progress dialog if a string was provided
{
// TODO: support cancellation?
//FScopedSourceControlProgress Progress(TaskText, FSimpleDelegate::CreateStatic(&Local::CancelCommand, &InCommand));
FScopedSourceControlProgress Progress(TaskText);
// Issue the command asynchronously...
IssueCommand( InCommand );
// ... then wait for its completion (thus making it synchronous)
while (!InCommand.IsCanceled() && CommandQueue.Contains(&InCommand))
{
// Tick the command queue and update progress.
Tick();
if (i >= 20) {
Progress.Tick();
i = 0;
}
i++;
// Sleep for a bit so we don't busy-wait so much.
FPlatformProcess::Sleep(0.01f);
}
if (InCommand.bCancelled)
{
Result = ECommandResult::Cancelled;
}
if (InCommand.bCommandSuccessful)
{
Result = ECommandResult::Succeeded;
}
else if (!bSuppressResponseMsg)
{
FMessageDialog::Open( EAppMsgType::Ok, LOCTEXT("Git_ServerUnresponsive", "Git command failed. Please check your connection and try again, or check the output log for more information.") );
UE_LOG(LogSourceControl, Error, TEXT("Command '%s' Failed!"), *InCommand.Operation->GetName().ToString());
}
}
// Delete the command now if not marked as auto-delete
if (!InCommand.bAutoDelete)
{
delete &InCommand;
}
return Result;
}
ECommandResult::Type FGitSourceControlProvider::IssueCommand(FGitSourceControlCommand& InCommand, const bool bSynchronous)
{
if (!bSynchronous && GThreadPool != nullptr)
{
// Queue this to our worker thread(s) for resolving.
// When asynchronous, any callback gets called from Tick().
GThreadPool->AddQueuedWork(&InCommand);
CommandQueue.Add(&InCommand);
return ECommandResult::Succeeded;
}
else
{
UE_LOG(LogSourceControl, Log, TEXT("There are no threads available to process the revision control command '%s'. Running synchronously."), *InCommand.Operation->GetName().ToString());
InCommand.bCommandSuccessful = InCommand.DoWork();
InCommand.Worker->UpdateStates();
OutputCommandMessages(InCommand);
// Callback now if present. When asynchronous, this callback gets called from Tick().
return InCommand.ReturnResults();
}
}
bool FGitSourceControlProvider::QueryStateBranchConfig(const FString& ConfigSrc, const FString& ConfigDest)
{
// Check similar preconditions to Perforce (valid src and dest),
if (ConfigSrc.Len() == 0 || ConfigDest.Len() == 0)
{
return false;
}
if (!bGitAvailable || !bGitRepositoryFound)
{
FTSMessageLog("SourceControl").Error(LOCTEXT("StatusBranchConfigNoConnection", "Unable to retrieve status branch configuration from repo, no connection"));
return false;
}
// Otherwise, we can assume that whatever our user is doing to config state branches is properly synced, so just copy.
// TODO: maybe don't assume, and use git show instead?
IFileManager::Get().Copy(*ConfigDest, *ConfigSrc);
return true;
}
void FGitSourceControlProvider::RegisterStateBranches(const TArray<FString>& BranchNames, const FString& ContentRootIn)
{
StatusBranchNamePatternsInternal = BranchNames;
}
int32 FGitSourceControlProvider::GetStateBranchIndex(const FString& StateBranchName) const
{
// How do state branches indices work?
// Order matters. Lower values are lower in the hierarchy, i.e., changes from higher branches get automatically merged down.
// The higher branch is, the stabler it is, and has changes manually promoted up.
// Check if we are checking the index of the current branch
// UE uses FEngineVersion for the current branch name because of UEGames setup, but we want to handle otherwise for Git repos.
auto StatusBranchNames = GetStatusBranchNames();
if (StateBranchName == FEngineVersion::Current().GetBranch())
{
const int32 CurrentBranchStatusIndex = StatusBranchNames.IndexOfByKey(BranchName);
const bool bCurrentBranchInStatusBranches = CurrentBranchStatusIndex != INDEX_NONE;
// If the user's current branch is tracked as a status branch, give the proper index
if (bCurrentBranchInStatusBranches)
{
return CurrentBranchStatusIndex;
}
// If the current branch is not a status branch, make it the highest branch
// This is semantically correct, since if a branch is not marked as a status branch
// it merges changes in a similar fashion to the highest status branch, i.e. manually promotes them
// based on the user merging those changes in. and these changes always get merged from even the highest point
// of the stream. i.e, promoted/stable changes are always up for consumption by this branch.
return INT32_MAX;
}
// If we're not checking the current branch, then we don't need to do special handling.
// If it is not a status branch, there is no message
return StatusBranchNames.IndexOfByKey(StateBranchName);
}
TArray<FString> FGitSourceControlProvider::GetStatusBranchNames() const
{
TArray<FString> StatusBranches;
if(PathToGitBinary.IsEmpty() || PathToRepositoryRoot.IsEmpty())
return StatusBranches;
for (int i = 0; i < StatusBranchNamePatternsInternal.Num(); i++)
{
TArray<FString> Matches;
bool bResult = GitSourceControlUtils::GetRemoteBranchesWildcard(PathToGitBinary, PathToRepositoryRoot, StatusBranchNamePatternsInternal[i], Matches);
if (bResult && Matches.Num() > 0)
{
for (int j = 0; j < Matches.Num(); j++)
{
StatusBranches.Add(Matches[j].TrimStartAndEnd());
}
}
}
return StatusBranches;
}
#undef LOCTEXT_NAMESPACE

View File

@@ -0,0 +1,308 @@
// 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)
#pragma once
#include "GitSourceControlChangelist.h"
#include "ISourceControlProvider.h"
#include "IGitSourceControlWorker.h"
#include "GitSourceControlMenu.h"
#include "Runtime/Launch/Resources/Version.h"
class FGitSourceControlChangelistState;
class FGitSourceControlState;
class FGitSourceControlCommand;
DECLARE_DELEGATE_RetVal(FGitSourceControlWorkerRef, FGetGitSourceControlWorker)
/// Git version and capabilites extracted from the string "git version 2.11.0.windows.3"
struct FGitVersion
{
// Git version extracted from the string "git version 2.11.0.windows.3" (Windows), "git version 2.11.0" (Linux/Mac/Cygwin/WSL) or "git version 2.31.1.vfs.0.3" (Microsoft)
int Major; // 2 Major version number
int Minor; // 31 Minor version number
int Patch; // 1 Patch/bugfix number
bool bIsFork;
FString Fork; // "vfs"
int ForkMajor; // 0 Fork specific revision number
int ForkMinor; // 3
int ForkPatch; // ?
FGitVersion()
: Major(0)
, Minor(0)
, Patch(0)
, bIsFork(false)
, ForkMajor(0)
, ForkMinor(0)
, ForkPatch(0)
{
}
};
class FGitSourceControlProvider : public ISourceControlProvider
{
public:
/* ISourceControlProvider implementation */
virtual void Init(bool bForceConnection = true) override;
virtual void Close() override;
virtual FText GetStatusText() const override;
virtual bool IsEnabled() const override;
virtual bool IsAvailable() const override;
virtual const FName& GetName(void) const override;
virtual bool QueryStateBranchConfig(const FString& ConfigSrc, const FString& ConfigDest) override;
virtual void RegisterStateBranches(const TArray<FString>& BranchNames, const FString& ContentRootIn) override;
virtual int32 GetStateBranchIndex(const FString& BranchName) const override;
virtual ECommandResult::Type GetState( const TArray<FString>& InFiles, TArray<FSourceControlStateRef>& OutState, EStateCacheUsage::Type InStateCacheUsage ) override;
#if ENGINE_MAJOR_VERSION >= 5
virtual ECommandResult::Type GetState(const TArray<FSourceControlChangelistRef>& InChangelists, TArray<FSourceControlChangelistStateRef>& OutState, EStateCacheUsage::Type InStateCacheUsage) override;
#endif
virtual TArray<FSourceControlStateRef> GetCachedStateByPredicate(TFunctionRef<bool(const FSourceControlStateRef&)> Predicate) const override;
virtual FDelegateHandle RegisterSourceControlStateChanged_Handle(const FSourceControlStateChanged::FDelegate& SourceControlStateChanged) override;
virtual void UnregisterSourceControlStateChanged_Handle(FDelegateHandle Handle) override;
#if ENGINE_MAJOR_VERSION < 5
virtual ECommandResult::Type Execute( const FSourceControlOperationRef& InOperation, const TArray<FString>& InFiles, EConcurrency::Type InConcurrency = EConcurrency::Synchronous, const FSourceControlOperationComplete& InOperationCompleteDelegate = FSourceControlOperationComplete()) override;
virtual bool CanCancelOperation( const FSourceControlOperationRef& InOperation ) const override;
virtual void CancelOperation( const FSourceControlOperationRef& InOperation ) override;
#else
virtual ECommandResult::Type Execute(const FSourceControlOperationRef& InOperation, FSourceControlChangelistPtr InChangelist, const TArray<FString>& InFiles, EConcurrency::Type InConcurrency = EConcurrency::Synchronous, const FSourceControlOperationComplete& InOperationCompleteDelegate = FSourceControlOperationComplete()) override;
virtual bool CanCancelOperation( const FSourceControlOperationRef& InOperation ) const override;
virtual void CancelOperation( const FSourceControlOperationRef& InOperation ) override;
#endif
virtual bool UsesLocalReadOnlyState() const override;
virtual bool UsesChangelists() const override;
virtual bool UsesCheckout() const override;
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
virtual bool UsesFileRevisions() const override;
virtual TOptional<bool> IsAtLatestRevision() const override;
virtual TOptional<int> GetNumLocalChanges() const override;
#endif
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2
virtual bool AllowsDiffAgainstDepot() const override;
virtual bool UsesUncontrolledChangelists() const override;
virtual bool UsesSnapshots() const override;
#endif
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
virtual bool CanExecuteOperation( const FSourceControlOperationRef& InOperation ) const override;
virtual TMap<EStatus, FString> GetStatus() const override;
#endif
virtual void Tick() override;
virtual TArray< TSharedRef<class ISourceControlLabel> > GetLabels( const FString& InMatchingSpec ) const override;
#if ENGINE_MAJOR_VERSION >= 5
virtual TArray<FSourceControlChangelistRef> GetChangelists( EStateCacheUsage::Type InStateCacheUsage ) override;
#endif
#if SOURCE_CONTROL_WITH_SLATE
virtual TSharedRef<class SWidget> MakeSettingsWidget() const override;
#endif
using ISourceControlProvider::Execute;
/**
* Check configuration, else standard paths, and run a Git "version" command to check the availability of the binary.
*/
void CheckGitAvailability();
/** Refresh Git settings from revision control settings */
void UpdateSettings();
/**
* Find the .git/ repository and check its status.
*/
void CheckRepositoryStatus();
/** Is git binary found and working. */
inline bool IsGitAvailable() const
{
return bGitAvailable;
}
/** Git version for feature checking */
inline const FGitVersion& GetGitVersion() const
{
return GitVersion;
}
/** Path to the root of the Unreal revision control repository: usually the ProjectDir */
inline const FString& GetPathToRepositoryRoot() const
{
return PathToRepositoryRoot;
}
/** Path to the root of the Git repository: can be the ProjectDir itself, or any parent directory (found by the "Connect" operation) */
inline const FString& GetPathToGitRoot() const
{
return PathToGitRoot;
}
/** Gets the path to the Git binary */
inline const FString& GetGitBinaryPath() const
{
return PathToGitBinary;
}
/** Git config user.name */
inline const FString& GetUserName() const
{
return UserName;
}
/** Git config user.email */
inline const FString& GetUserEmail() const
{
return UserEmail;
}
/** Git remote origin url */
inline const FString& GetRemoteUrl() const
{
return RemoteUrl;
}
inline const FString& GetLockUser() const
{
return LockUser;
}
/** Helper function used to update state cache */
TSharedRef<FGitSourceControlState, ESPMode::ThreadSafe> GetStateInternal(const FString& Filename);
/** Helper function used to update changelists state cache */
TSharedRef<FGitSourceControlChangelistState, ESPMode::ThreadSafe> GetStateInternal(const FGitSourceControlChangelist& InChangelist);
/**
* Register a worker with the provider.
* This is used internally so the provider can maintain a map of all available operations.
*/
void RegisterWorker( const FName& InName, const FGetGitSourceControlWorker& InDelegate );
/** Set list of error messages that occurred after last perforce command */
void SetLastErrors(const TArray<FText>& InErrors);
/** Get list of error messages that occurred after last perforce command */
TArray<FText> GetLastErrors() const;
/** Get number of error messages seen after running last perforce command */
int32 GetNumLastErrors() const;
/** Remove a named file from the state cache */
bool RemoveFileFromCache(const FString& Filename);
/** Get files in cache */
TArray<FString> GetFilesInCache();
bool AddFileToIgnoreForceCache(const FString& Filename);
bool RemoveFileFromIgnoreForceCache(const FString& Filename);
const FString& GetBranchName() const
{
return BranchName;
}
const FString& GetRemoteBranchName() const { return RemoteBranchName; }
TArray<FString> GetStatusBranchNames() const;
/** Indicates editor binaries are to be updated upon next sync */
bool bPendingRestart;
#if ENGINE_MAJOR_VERSION >= 5
uint32 TicksUntilNextForcedUpdate = 0;
#endif
private:
/** Is git binary found and working. */
bool bGitAvailable = false;
/** Is git repository found. */
bool bGitRepositoryFound = false;
/** Is LFS locking enabled? */
bool bUsingGitLfsLocking = false;
FString PathToGitBinary;
FString LockUser;
/** Critical section for thread safety of error messages that occurred after last perforce command */
mutable FCriticalSection LastErrorsCriticalSection;
/** List of error messages that occurred after last perforce command */
TArray<FText> LastErrors;
/** Helper function for Execute() */
TSharedPtr<class IGitSourceControlWorker, ESPMode::ThreadSafe> CreateWorker(const FName& InOperationName) const;
/** Helper function for running command synchronously. */
ECommandResult::Type ExecuteSynchronousCommand(class FGitSourceControlCommand& InCommand, const FText& Task, bool bSuppressResponseMsg);
/** Issue a command asynchronously if possible. */
ECommandResult::Type IssueCommand(class FGitSourceControlCommand& InCommand, const bool bSynchronous = false );
/** Output any messages this command holds */
void OutputCommandMessages(const class FGitSourceControlCommand& InCommand) const;
/** Update repository status on Connect and UpdateStatus operations */
void UpdateRepositoryStatus(const class FGitSourceControlCommand& InCommand);
/** Path to the root of the Unreal revision control repository: usually the ProjectDir */
FString PathToRepositoryRoot;
/** Path to the root of the Git repository: can be the ProjectDir itself, or any parent directory (found by the "Connect" operation) */
FString PathToGitRoot;
/** Git config user.name (from local repository, else globally) */
FString UserName;
/** Git config user.email (from local repository, else globally) */
FString UserEmail;
/** Name of the current branch */
FString BranchName;
/** Name of the current remote branch */
FString RemoteBranchName;
/** URL of the "origin" default remote server */
FString RemoteUrl;
/** Current Commit full SHA1 */
FString CommitId;
/** Current Commit description's Summary */
FString CommitSummary;
/** State cache */
TMap<FString, TSharedRef<class FGitSourceControlState, ESPMode::ThreadSafe> > StateCache;
TMap<FGitSourceControlChangelist, TSharedRef<class FGitSourceControlChangelistState, ESPMode::ThreadSafe> > ChangelistsStateCache;
/** The currently registered revision control operations */
TMap<FName, FGetGitSourceControlWorker> WorkersMap;
/** Queue for commands given by the main thread */
TArray < FGitSourceControlCommand* > CommandQueue;
/** For notifying when the revision control states in the cache have changed */
FSourceControlStateChanged OnSourceControlStateChanged;
/** Git version for feature checking */
FGitVersion GitVersion;
/** Revision Control Menu Extension */
FGitSourceControlMenu GitSourceControlMenu;
/**
Ignore these files when forcing status updates. We add to this list when we've just updated the status already.
UE's SourceControl has a habit of performing a double status update, immediately after an operation.
*/
TArray<FString> IgnoreForceCache;
/** Array of branch name patterns for status queries */
TArray<FString> StatusBranchNamePatternsInternal;
class FGitSourceControlRunner* Runner = nullptr;
};

View File

@@ -0,0 +1,134 @@
// Copyright (c) 2014-2023 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 "GitSourceControlRevision.h"
#include "HAL/FileManager.h"
#include "Misc/Paths.h"
#include "Modules/ModuleManager.h"
#include "GitSourceControlModule.h"
#include "GitSourceControlUtils.h"
#include "ISourceControlModule.h"
#define LOCTEXT_NAMESPACE "GitSourceControl"
#if ENGINE_MAJOR_VERSION >= 5
bool FGitSourceControlRevision::Get( FString& InOutFilename, EConcurrency::Type InConcurrency ) const
{
if (InConcurrency != EConcurrency::Synchronous)
{
UE_LOG(LogSourceControl, Warning, TEXT("Only EConcurrency::Synchronous is tested/supported for this operation."));
}
#else
bool FGitSourceControlRevision::Get( FString& InOutFilename ) const
{
#endif
const FGitSourceControlModule* GitSourceControl = FGitSourceControlModule::GetThreadSafe();
if (!GitSourceControl)
{
return false;
}
const FGitSourceControlProvider& Provider = GitSourceControl->GetProvider();
const FString PathToGitBinary = Provider.GetGitBinaryPath();
FString PathToRepositoryRoot = Provider.GetPathToRepositoryRoot();
// the repo root can be customised if in a plugin that has it's own repo
if (PathToRepoRoot.Len())
{
PathToRepositoryRoot = PathToRepoRoot;
}
// if a filename for the temp file wasn't supplied generate a unique-ish one
if(InOutFilename.Len() == 0)
{
// create the diff dir if we don't already have it (Git wont)
IFileManager::Get().MakeDirectory(*FPaths::DiffDir(), true);
// create a unique temp file name based on the unique commit Id
const FString TempFileName = FString::Printf(TEXT("%stemp-%s-%s"), *FPaths::DiffDir(), *CommitId, *FPaths::GetCleanFilename(Filename));
InOutFilename = FPaths::ConvertRelativePathToFull(TempFileName);
}
// Diff against the revision
const FString Parameter = FString::Printf(TEXT("%s:%s"), *CommitId, *Filename);
bool bCommandSuccessful;
if(FPaths::FileExists(InOutFilename))
{
bCommandSuccessful = true; // if the temp file already exists, reuse it directly
}
else
{
bCommandSuccessful = GitSourceControlUtils::RunDumpToFile(PathToGitBinary, PathToRepositoryRoot, Parameter, InOutFilename);
}
return bCommandSuccessful;
}
bool FGitSourceControlRevision::GetAnnotated( TArray<FAnnotationLine>& OutLines ) const
{
return false;
}
bool FGitSourceControlRevision::GetAnnotated( FString& InOutFilename ) const
{
return false;
}
const FString& FGitSourceControlRevision::GetFilename() const
{
return Filename;
}
int32 FGitSourceControlRevision::GetRevisionNumber() const
{
return RevisionNumber;
}
const FString& FGitSourceControlRevision::GetRevision() const
{
return ShortCommitId;
}
const FString& FGitSourceControlRevision::GetDescription() const
{
return Description;
}
const FString& FGitSourceControlRevision::GetUserName() const
{
return UserName;
}
const FString& FGitSourceControlRevision::GetClientSpec() const
{
static FString EmptyString(TEXT(""));
return EmptyString;
}
const FString& FGitSourceControlRevision::GetAction() const
{
return Action;
}
TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FGitSourceControlRevision::GetBranchSource() const
{
// if this revision was copied/moved from some other revision
return BranchSource;
}
const FDateTime& FGitSourceControlRevision::GetDate() const
{
return Date;
}
int32 FGitSourceControlRevision::GetCheckInIdentifier() const
{
return CommitIdNumber;
}
int32 FGitSourceControlRevision::GetFileSize() const
{
return FileSize;
}
#undef LOCTEXT_NAMESPACE

View File

@@ -0,0 +1,80 @@
// Copyright (c) 2014-2023 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)
#pragma once
#include "ISourceControlRevision.h"
#include "Runtime/Launch/Resources/Version.h"
#include "Misc/DateTime.h"
/** Revision of a file, linked to a specific commit */
class FGitSourceControlRevision : public ISourceControlRevision
{
public:
/** ISourceControlRevision interface */
#if ENGINE_MAJOR_VERSION >= 5
virtual bool Get( FString& InOutFilename, EConcurrency::Type InConcurrency = EConcurrency::Synchronous ) const override;
#else
virtual bool Get( FString& InOutFilename ) const override;
#endif
virtual bool GetAnnotated( TArray<FAnnotationLine>& OutLines ) const override;
virtual bool GetAnnotated( FString& InOutFilename ) const override;
virtual const FString& GetFilename() const override;
virtual int32 GetRevisionNumber() const override;
virtual const FString& GetRevision() const override;
virtual const FString& GetDescription() const override;
virtual const FString& GetUserName() const override;
virtual const FString& GetClientSpec() const override;
virtual const FString& GetAction() const override;
virtual TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> GetBranchSource() const override;
virtual const FDateTime& GetDate() const override;
virtual int32 GetCheckInIdentifier() const override;
virtual int32 GetFileSize() const override;
public:
/** The filename this revision refers to */
FString Filename;
/** The full hexadecimal SHA1 id of the commit this revision refers to */
FString CommitId;
/** The short hexadecimal SHA1 id (8 first hex char out of 40) of the commit: the string to display */
FString ShortCommitId;
/** The numeric value of the short SHA1 (8 first hex char out of 40) */
int32 CommitIdNumber = 0;
/** The index of the revision in the history (SBlueprintRevisionMenu assumes order for the "Depot" label) */
int32 RevisionNumber = 0;
/** The SHA1 identifier of the file at this revision */
FString FileHash;
/** The description of this revision */
FString Description;
/** The user that made the change */
FString UserName;
/** The action (add, edit, branch etc.) performed at this revision */
FString Action;
/** Source of move ("branch" in Perforce term) if any */
TSharedPtr<FGitSourceControlRevision, ESPMode::ThreadSafe> BranchSource;
/** The date this revision was made */
FDateTime Date;
/** The size of the file at this revision */
int32 FileSize;
/** Dynamic repository root **/
FString PathToRepoRoot;
};
/** History composed of the last 100 revisions of the file */
typedef TArray< TSharedRef<FGitSourceControlRevision, ESPMode::ThreadSafe> > TGitSourceControlHistory;

View File

@@ -0,0 +1,95 @@
// Copyright Project Borealis
#include "GitSourceControlRunner.h"
#include "GitSourceControlModule.h"
#include "GitSourceControlProvider.h"
#include "GitSourceControlOperations.h"
#include "Async/Async.h"
FGitSourceControlRunner::FGitSourceControlRunner()
{
bRunThread = true;
bRefreshSpawned = false;
StopEvent = FPlatformProcess::GetSynchEventFromPool(true);
Thread = FRunnableThread::Create(this, TEXT("GitSourceControlRunner"));
}
FGitSourceControlRunner::~FGitSourceControlRunner()
{
if (Thread)
{
Thread->Kill();
delete StopEvent;
delete Thread;
}
}
bool FGitSourceControlRunner::Init()
{
return true;
}
uint32 FGitSourceControlRunner::Run()
{
while (bRunThread)
{
StopEvent->Wait(30000);
if (!bRunThread)
{
break;
}
// If we're not running the task already
if (!bRefreshSpawned)
{
// Flag that we're running the task already
bRefreshSpawned = true;
const auto ExecuteResult = Async(EAsyncExecution::TaskGraphMainThread, [this] {
FGitSourceControlModule* GitSourceControl = FGitSourceControlModule::GetThreadSafe();
// Module not loaded, bail. Usually happens when editor is shutting down, and this prevents a crash from bad timing.
if (!GitSourceControl)
{
return ECommandResult::Failed;
}
FGitSourceControlProvider& Provider = GitSourceControl->GetProvider();
TSharedRef<FGitFetch, ESPMode::ThreadSafe> RefreshOperation = ISourceControlOperation::Create<FGitFetch>();
RefreshOperation->bUpdateStatus = true;
#if ENGINE_MAJOR_VERSION >= 5
const ECommandResult::Type Result = Provider.Execute(RefreshOperation, FSourceControlChangelistPtr(), FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous,
FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlRunner::OnSourceControlOperationComplete));
#else
const ECommandResult::Type Result = Provider.Execute(RefreshOperation, FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous,
FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlRunner::OnSourceControlOperationComplete));
#endif
return Result;
});
// Wait for result if not already completed
if (bRefreshSpawned && bRunThread)
{
// Get the result
ECommandResult::Type Result = ExecuteResult.Get();
// If still not completed,
if (bRefreshSpawned)
{
// mark failures as done, successes have to complete
bRefreshSpawned = Result == ECommandResult::Succeeded;
}
}
}
}
return 0;
}
void FGitSourceControlRunner::Stop()
{
bRunThread = false;
StopEvent->Trigger();
}
void FGitSourceControlRunner::OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult)
{
// Mark task as done
bRefreshSpawned = false;
}

View File

@@ -0,0 +1,34 @@
// Copyright Project Borealis
#pragma once
#include "CoreMinimal.h"
#include "HAL/Runnable.h"
#include "ISourceControlProvider.h"
#include "ISourceControlOperation.h"
/**
*
*/
class FGitSourceControlRunner : public FRunnable
{
public:
FGitSourceControlRunner();
// Destructor
virtual ~FGitSourceControlRunner() override;
bool Init() override;
uint32 Run() override;
void Stop() override;
void OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult);
private:
FRunnableThread* Thread;
FEvent* StopEvent;
bool bRunThread;
bool bRefreshSpawned;
};

View File

@@ -0,0 +1,86 @@
// 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 "GitSourceControlSettings.h"
#include "Misc/ConfigCacheIni.h"
#include "SourceControlHelpers.h"
namespace GitSettingsConstants
{
/** The section of the ini file we load our settings from */
static const FString SettingsSection = TEXT("GitSourceControl.GitSourceControlSettings");
}
const FString FGitSourceControlSettings::GetBinaryPath() const
{
FScopeLock ScopeLock(&CriticalSection);
return BinaryPath; // Return a copy to be thread-safe
}
bool FGitSourceControlSettings::SetBinaryPath(const FString& InString)
{
FScopeLock ScopeLock(&CriticalSection);
const bool bChanged = (BinaryPath != InString);
if(bChanged)
{
BinaryPath = InString;
}
return bChanged;
}
/** Tell if using the Git LFS file Locking workflow */
bool FGitSourceControlSettings::IsUsingGitLfsLocking() const
{
FScopeLock ScopeLock(&CriticalSection);
return bUsingGitLfsLocking;
}
/** Configure the usage of Git LFS file Locking workflow */
bool FGitSourceControlSettings::SetUsingGitLfsLocking(const bool InUsingGitLfsLocking)
{
FScopeLock ScopeLock(&CriticalSection);
const bool bChanged = (bUsingGitLfsLocking != InUsingGitLfsLocking);
bUsingGitLfsLocking = InUsingGitLfsLocking;
return bChanged;
}
const FString FGitSourceControlSettings::GetLfsUserName() const
{
FScopeLock ScopeLock(&CriticalSection);
return LfsUserName; // Return a copy to be thread-safe
}
bool FGitSourceControlSettings::SetLfsUserName(const FString& InString)
{
FScopeLock ScopeLock(&CriticalSection);
const bool bChanged = (LfsUserName != InString);
if (bChanged)
{
LfsUserName = InString;
}
return bChanged;
}
// This is called at startup nearly before anything else in our module: BinaryPath will then be used by the provider
void FGitSourceControlSettings::LoadSettings()
{
FScopeLock ScopeLock(&CriticalSection);
const FString& IniFile = SourceControlHelpers::GetSettingsIni();
GConfig->GetString(*GitSettingsConstants::SettingsSection, TEXT("BinaryPath"), BinaryPath, IniFile);
GConfig->GetBool(*GitSettingsConstants::SettingsSection, TEXT("UsingGitLfsLocking"), bUsingGitLfsLocking, IniFile);
GConfig->GetString(*GitSettingsConstants::SettingsSection, TEXT("LfsUserName"), LfsUserName, IniFile);
}
void FGitSourceControlSettings::SaveSettings() const
{
FScopeLock ScopeLock(&CriticalSection);
const FString& IniFile = SourceControlHelpers::GetSettingsIni();
GConfig->SetString(*GitSettingsConstants::SettingsSection, TEXT("BinaryPath"), *BinaryPath, IniFile);
GConfig->SetBool(*GitSettingsConstants::SettingsSection, TEXT("UsingGitLfsLocking"), bUsingGitLfsLocking, IniFile);
GConfig->SetString(*GitSettingsConstants::SettingsSection, TEXT("LfsUserName"), *LfsUserName, IniFile);
}

View File

@@ -0,0 +1,50 @@
// 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)
#pragma once
#include "Containers/UnrealString.h"
#include "HAL/CriticalSection.h"
class FGitSourceControlSettings
{
public:
/** Get the Git Binary Path */
const FString GetBinaryPath() const;
/** Set the Git Binary Path */
bool SetBinaryPath(const FString& InString);
/** Tell if using the Git LFS file Locking workflow */
bool IsUsingGitLfsLocking() const;
/** Configure the usage of Git LFS file Locking workflow */
bool SetUsingGitLfsLocking(const bool InUsingGitLfsLocking);
/** Get the username used by the Git LFS 2 File Locks server */
const FString GetLfsUserName() const;
/** Set the username used by the Git LFS 2 File Locks server */
bool SetLfsUserName(const FString& InString);
/** Load settings from ini file */
void LoadSettings();
/** Save settings to ini file */
void SaveSettings() const;
private:
/** A critical section for settings access */
mutable FCriticalSection CriticalSection;
/** Git binary path */
FString BinaryPath;
/** Tells if using the Git LFS file Locking workflow */
bool bUsingGitLfsLocking = true;
/** Username used by the Git LFS 2 File Locks server */
FString LfsUserName;
};

View File

@@ -0,0 +1,461 @@
// 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 "GitSourceControlState.h"
#if ENGINE_MAJOR_VERSION >= 5
#include "Textures/SlateIcon.h"
#if ENGINE_MINOR_VERSION >= 2
#include "RevisionControlStyle/RevisionControlStyle.h"
#endif
#endif
#define LOCTEXT_NAMESPACE "GitSourceControl.State"
int32 FGitSourceControlState::GetHistorySize() const
{
return History.Num();
}
TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FGitSourceControlState::GetHistoryItem( int32 HistoryIndex ) const
{
check(History.IsValidIndex(HistoryIndex));
return History[HistoryIndex];
}
TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FGitSourceControlState::FindHistoryRevision(int32 RevisionNumber) const
{
for (auto Iter(History.CreateConstIterator()); Iter; Iter++)
{
if ((*Iter)->GetRevisionNumber() == RevisionNumber)
{
return *Iter;
}
}
return nullptr;
}
TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FGitSourceControlState::FindHistoryRevision(const FString& InRevision) const
{
for (const auto& Revision : History)
{
if (Revision->GetRevision() == InRevision)
{
return Revision;
}
}
return nullptr;
}
#if ENGINE_MAJOR_VERSION < 5 || ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 3
TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FGitSourceControlState::GetBaseRevForMerge() const
{
for(const auto& Revision : History)
{
// look for the the SHA1 id of the file, not the commit id (revision)
if (Revision->FileHash == PendingMergeBaseFileHash)
{
return Revision;
}
}
return nullptr;
}
#endif
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2
TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FGitSourceControlState::GetCurrentRevision() const
{
return nullptr;
}
#endif
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
ISourceControlState::FResolveInfo FGitSourceControlState::GetResolveInfo() const
{
return PendingResolveInfo;
}
#endif
// @todo add Slate icons for git specific states (NotAtHead vs Conflicted...)
#if ENGINE_MAJOR_VERSION < 5
#define GET_ICON_RETURN( NAME ) FName( "ContentBrowser.SCC_" #NAME )
FName FGitSourceControlState::GetIconName() const
{
#else
#if ENGINE_MINOR_VERSION >= 2
#define GET_ICON_RETURN( NAME ) FSlateIcon(FRevisionControlStyleManager::GetStyleSetName(), "RevisionControl." #NAME )
#else
#define GET_ICON_RETURN( NAME ) FSlateIcon(FAppStyle::GetAppStyleSetName(), "Perforce." #NAME )
#endif
FSlateIcon FGitSourceControlState::GetIcon() const
{
#endif
switch (GetGitState())
{
case EGitState::NotAtHead:
return GET_ICON_RETURN(NotAtHeadRevision);
case EGitState::LockedOther:
return GET_ICON_RETURN(CheckedOutByOtherUser);
case EGitState::NotLatest:
return GET_ICON_RETURN(ModifiedOtherBranch);
case EGitState::Unmerged:
return GET_ICON_RETURN(Branched);
case EGitState::Added:
return GET_ICON_RETURN(OpenForAdd);
case EGitState::Untracked:
return GET_ICON_RETURN(NotInDepot);
case EGitState::Deleted:
return GET_ICON_RETURN(MarkedForDelete);
case EGitState::Modified:
case EGitState::CheckedOut:
return GET_ICON_RETURN(CheckedOut);
case EGitState::Ignored:
return GET_ICON_RETURN(NotInDepot);
default:
#if ENGINE_MAJOR_VERSION < 5
return NAME_None;
#else
return FSlateIcon();
#endif
}
}
#if ENGINE_MAJOR_VERSION < 5
FName FGitSourceControlState::GetSmallIconName() const
{
switch (GetGitState()) {
case EGitState::NotAtHead:
return FName("ContentBrowser.SCC_NotAtHeadRevision_Small");
case EGitState::LockedOther:
return FName("ContentBrowser.SCC_CheckedOutByOtherUser_Small");
case EGitState::NotLatest:
return FName("ContentBrowser.SCC_ModifiedOtherBranch_Small");
case EGitState::Unmerged:
return FName("ContentBrowser.SCC_Branched_Small");
case EGitState::Added:
return FName("ContentBrowser.SCC_OpenForAdd_Small");
case EGitState::Untracked:
return FName("ContentBrowser.SCC_NotInDepot_Small");
case EGitState::Deleted:
return FName("ContentBrowser.SCC_MarkedForDelete_Small");
case EGitState::Modified:
case EGitState::CheckedOut:
return FName("ContentBrowser.SCC_CheckedOut_Small");
case EGitState::Ignored:
return FName("ContentBrowser.SCC_NotInDepot_Small");
default:
return NAME_None;
}
}
#endif
FText FGitSourceControlState::GetDisplayName() const
{
switch (GetGitState())
{
case EGitState::NotAtHead:
return LOCTEXT("NotCurrent", "Not current");
case EGitState::LockedOther:
return FText::Format(LOCTEXT("CheckedOutOther", "Checked out by: {0}"), FText::FromString(State.LockUser));
case EGitState::NotLatest:
return FText::Format(LOCTEXT("ModifiedOtherBranch", "Modified in branch: {0}"), FText::FromString(State.HeadBranch));
case EGitState::Unmerged:
return LOCTEXT("Conflicted", "Conflicted");
case EGitState::Added:
return LOCTEXT("OpenedForAdd", "Opened for add");
case EGitState::Untracked:
return LOCTEXT("NotControlled", "Not Under Revision Control");
case EGitState::Deleted:
return LOCTEXT("MarkedForDelete", "Marked for delete");
case EGitState::Modified:
case EGitState::CheckedOut:
return LOCTEXT("CheckedOut", "Checked out");
case EGitState::Ignored:
return LOCTEXT("Ignore", "Ignore");
case EGitState::Lockable:
return LOCTEXT("ReadOnly", "Read only");
case EGitState::None:
return LOCTEXT("Unknown", "Unknown");
default:
return FText();
}
}
FText FGitSourceControlState::GetDisplayTooltip() const
{
switch (GetGitState())
{
case EGitState::NotAtHead:
return LOCTEXT("NotCurrent_Tooltip", "The file(s) are not at the head revision");
case EGitState::LockedOther:
return FText::Format(LOCTEXT("CheckedOutOther_Tooltip", "Checked out by: {0}"), FText::FromString(State.LockUser));
case EGitState::NotLatest:
return FText::Format(LOCTEXT("ModifiedOtherBranch_Tooltip", "Modified in branch: {0} CL:{1} ({2})"), FText::FromString(State.HeadBranch), FText::FromString(HeadCommit), FText::FromString(HeadAction));
case EGitState::Unmerged:
return LOCTEXT("ContentsConflict_Tooltip", "The contents of the item conflict with updates received from the repository.");
case EGitState::Added:
return LOCTEXT("OpenedForAdd_Tooltip", "The file(s) are opened for add");
case EGitState::Untracked:
return LOCTEXT("NotControlled_Tooltip", "Item is not under revision control.");
case EGitState::Deleted:
return LOCTEXT("MarkedForDelete_Tooltip", "The file(s) are marked for delete");
case EGitState::Modified:
case EGitState::CheckedOut:
return LOCTEXT("CheckedOut_Tooltip", "The file(s) are checked out");
case EGitState::Ignored:
return LOCTEXT("Ignored_Tooltip", "Item is being ignored.");
case EGitState::Lockable:
return LOCTEXT("ReadOnly_Tooltip", "The file(s) are marked locally as read-only");
case EGitState::None:
return LOCTEXT("Unknown_Tooltip", "Unknown revision control state");
default:
return FText();
}
}
const FString& FGitSourceControlState::GetFilename() const
{
return LocalFilename;
}
const FDateTime& FGitSourceControlState::GetTimeStamp() const
{
return TimeStamp;
}
// Deleted and Missing assets cannot appear in the Content Browser, but they do in the Submit files to Revision Control window!
bool FGitSourceControlState::CanCheckIn() const
{
// We can check in if this is new content
if (IsAdded())
{
return true;
}
// Cannot check back in if conflicted or not current
if (!IsCurrent() || IsConflicted())
{
return false;
}
// We can check back in if we're locked.
if (State.LockState == ELockState::Locked)
{
return true;
}
// We can check in any file that has been modified, unless someone else locked it.
if (State.LockState != ELockState::LockedOther && IsModified() && IsSourceControlled())
{
return true;
}
return false;
}
bool FGitSourceControlState::CanCheckout() const
{
if (State.LockState == ELockState::Unlockable)
{
// Everything is already available for check in (checked out).
return false;
}
else
{
// We don't want to allow checkout if the file is out-of-date, as modifying an out-of-date binary file will most likely result in a merge conflict
return State.LockState == ELockState::NotLocked && IsCurrent();
}
}
bool FGitSourceControlState::IsCheckedOut() const
{
if (State.LockState == ELockState::Unlockable)
{
return IsSourceControlled(); // TODO: try modified instead? might block editing the file with a holding pattern
}
else
{
// We check for modified here too, because sometimes you don't lock a file but still want to push it. CanCheckout still true, so that you can lock it later...
return State.LockState == ELockState::Locked || (State.FileState == EFileState::Modified && State.LockState != ELockState::LockedOther);
}
}
bool FGitSourceControlState::IsCheckedOutOther(FString* Who) const
{
if (Who != nullptr)
{
// The packages dialog uses our lock user regardless if it was locked by other or us.
// But, if there is no lock user, it shows information about modification in other branches, which is important.
// So, only show our own lock user if it hasn't been modified in another branch.
// This is a very, very rare state (maybe impossible), but one that should be displayed properly.
if (State.LockState == ELockState::LockedOther || (State.LockState == ELockState::Locked && !IsModifiedInOtherBranch()))
{
*Who = State.LockUser;
}
}
return State.LockState == ELockState::LockedOther;
}
bool FGitSourceControlState::IsCheckedOutInOtherBranch(const FString& CurrentBranch) const
{
// You can't check out separately per branch
return false;
}
bool FGitSourceControlState::IsModifiedInOtherBranch(const FString& CurrentBranch) const
{
return State.RemoteState == ERemoteState::NotLatest;
}
bool FGitSourceControlState::GetOtherBranchHeadModification(FString& HeadBranchOut, FString& ActionOut, int32& HeadChangeListOut) const
{
if (!IsModifiedInOtherBranch())
{
return false;
}
HeadBranchOut = State.HeadBranch;
ActionOut = HeadAction; // TODO: from ERemoteState
HeadChangeListOut = 0; // TODO: get head commit
return true;
}
bool FGitSourceControlState::IsCurrent() const
{
return State.RemoteState != ERemoteState::NotAtHead && State.RemoteState != ERemoteState::NotLatest;
}
bool FGitSourceControlState::IsSourceControlled() const
{
return State.TreeState != ETreeState::Untracked && State.TreeState != ETreeState::Ignored && State.TreeState != ETreeState::NotInRepo;
}
bool FGitSourceControlState::IsAdded() const
{
// Added is when a file was untracked and is now added.
return State.FileState == EFileState::Added;
}
bool FGitSourceControlState::IsDeleted() const
{
return State.FileState == EFileState::Deleted;
}
bool FGitSourceControlState::IsIgnored() const
{
return State.TreeState == ETreeState::Ignored;
}
bool FGitSourceControlState::CanEdit() const
{
// Perforce does not care about it being current
return IsCheckedOut() || IsAdded();
}
bool FGitSourceControlState::CanDelete() const
{
// Perforce enforces that a deleted file must be current.
if (!IsCurrent())
{
return false;
}
// If someone else hasn't checked it out, we can delete revision controlled files.
return !IsCheckedOutOther() && IsSourceControlled();
}
bool FGitSourceControlState::IsUnknown() const
{
return State.FileState == EFileState::Unknown && State.TreeState == ETreeState::NotInRepo;
}
bool FGitSourceControlState::IsModified() const
{
return State.TreeState == ETreeState::Working ||
State.TreeState == ETreeState::Staged;
}
bool FGitSourceControlState::CanAdd() const
{
return State.TreeState == ETreeState::Untracked;
}
bool FGitSourceControlState::IsConflicted() const
{
return State.FileState == EFileState::Unmerged;
}
bool FGitSourceControlState::CanRevert() const
{
// Can revert the file state if we modified, even if it was locked by someone else.
// Useful for when someone locked a file, and you just wanna play around with it locallly, and then revert it.
return CanCheckIn() || IsModified();
}
EGitState::Type FGitSourceControlState::GetGitState() const
{
// No matter what, we must pull from remote, even if we have locked or if we have modified.
switch (State.RemoteState)
{
case ERemoteState::NotAtHead:
return EGitState::NotAtHead;
default:
break;
}
/** Someone else locked this file across branches. */
// We cannot push under any circumstance, if someone else has locked.
if (State.LockState == ELockState::LockedOther)
{
return EGitState::LockedOther;
}
// We could theoretically push, but we shouldn't.
if (State.RemoteState == ERemoteState::NotLatest)
{
return EGitState::NotLatest;
}
switch (State.FileState)
{
case EFileState::Unmerged:
return EGitState::Unmerged;
case EFileState::Added:
return EGitState::Added;
case EFileState::Deleted:
return EGitState::Deleted;
case EFileState::Modified:
return EGitState::Modified;
default:
break;
}
if (State.TreeState == ETreeState::Untracked)
{
return EGitState::Untracked;
}
if (State.LockState == ELockState::Locked)
{
return EGitState::CheckedOut;
}
if (IsSourceControlled())
{
if (CanCheckout())
{
return EGitState::Lockable;
}
return EGitState::Unmodified;
}
return EGitState::None;
}
#undef LOCTEXT_NAMESPACE

View File

@@ -0,0 +1,220 @@
// 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)
#pragma once
#include "GitSourceControlChangelist.h"
#include "GitSourceControlRevision.h"
#include "Runtime/Launch/Resources/Version.h"
/** A consolidation of state priorities. */
namespace EGitState
{
enum Type
{
Unset,
NotAtHead,
#if 0
AddedAtHead,
DeletedAtHead,
#endif
LockedOther,
NotLatest,
/** Unmerged state (modified, but conflicts) */
Unmerged,
Added,
Deleted,
Modified,
/** Not modified, but locked explicitly. */
CheckedOut,
Untracked,
Lockable,
Unmodified,
Ignored,
/** Whatever else. */
None,
};
}
/** Corresponds to diff file states. */
namespace EFileState
{
enum Type
{
Unset,
Unknown,
Added,
Copied,
Deleted,
Modified,
Renamed,
Missing,
Unmerged,
};
}
/** Where in the world is this file? */
namespace ETreeState
{
enum Type
{
Unset,
/** This file is synced to commit */
Unmodified,
/** This file is modified, but not in staging tree */
Working,
/** This file is in staging tree (git add) */
Staged,
/** This file is not tracked in the repo yet */
Untracked,
/** This file is ignored by the repo */
Ignored,
/** This file is outside the repo folder */
NotInRepo,
};
}
/** LFS locks status of this file */
namespace ELockState
{
enum Type
{
Unset,
Unknown,
Unlockable,
NotLocked,
Locked,
LockedOther,
};
}
/** What is this file doing at HEAD? */
namespace ERemoteState
{
enum Type
{
Unset,
/** Up to date */
UpToDate,
/** Local version is behind remote */
NotAtHead,
#if 0
// TODO: handle these
/** Remote file does not exist on local */
AddedAtHead,
/** Local was deleted on remote */
DeletedAtHead,
#endif
/** Not at the latest revision amongst the tracked branches */
NotLatest,
};
}
/** Combined state, for updating cache in a map. */
struct FGitState
{
EFileState::Type FileState = EFileState::Unknown;
ETreeState::Type TreeState = ETreeState::NotInRepo;
ELockState::Type LockState = ELockState::Unknown;
/** Name of user who has locked the file */
FString LockUser;
ERemoteState::Type RemoteState = ERemoteState::UpToDate;
/** The branch with the latest commit for this file */
FString HeadBranch;
};
class FGitSourceControlState : public ISourceControlState
{
public:
FGitSourceControlState(const FString &InLocalFilename)
: LocalFilename(InLocalFilename)
, TimeStamp(0)
, HeadAction(TEXT("Changed"))
, HeadCommit(TEXT("Unknown"))
{
}
/** ISourceControlState interface */
virtual int32 GetHistorySize() const override;
virtual TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> GetHistoryItem(int32 HistoryIndex) const override;
virtual TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FindHistoryRevision(int32 RevisionNumber) const override;
virtual TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FindHistoryRevision(const FString& InRevision) const override;
#if ENGINE_MAJOR_VERSION < 5 || ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 3
virtual TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> GetBaseRevForMerge() const override;
#else
virtual FResolveInfo GetResolveInfo() const override;
#endif
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2
virtual TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> GetCurrentRevision() const override;
#endif
#if ENGINE_MAJOR_VERSION >= 5
virtual FSlateIcon GetIcon() const override;
#else
virtual FName GetIconName() const override;
virtual FName GetSmallIconName() const override;
#endif
virtual FText GetDisplayName() const override;
virtual FText GetDisplayTooltip() const override;
virtual const FString& GetFilename() const override;
virtual const FDateTime& GetTimeStamp() const override;
virtual bool CanCheckIn() const override;
virtual bool CanCheckout() const override;
virtual bool IsCheckedOut() const override;
virtual bool IsCheckedOutOther(FString* Who = NULL) const override;
virtual bool IsCheckedOutInOtherBranch(const FString& CurrentBranch = FString()) const override;
virtual bool IsModifiedInOtherBranch(const FString& CurrentBranch = FString()) const override;
virtual bool IsCheckedOutOrModifiedInOtherBranch(const FString& CurrentBranch = FString()) const override { return IsModifiedInOtherBranch(CurrentBranch); }
virtual TArray<FString> GetCheckedOutBranches() const override { return TArray<FString>(); }
virtual FString GetOtherUserBranchCheckedOuts() const override { return FString(); }
virtual bool GetOtherBranchHeadModification(FString& HeadBranchOut, FString& ActionOut, int32& HeadChangeListOut) const override;
virtual bool IsCurrent() const override;
virtual bool IsSourceControlled() const override;
virtual bool IsAdded() const override;
virtual bool IsDeleted() const override;
virtual bool IsIgnored() const override;
virtual bool CanEdit() const override;
virtual bool IsUnknown() const override;
virtual bool IsModified() const override;
virtual bool CanAdd() const override;
virtual bool CanDelete() const override;
virtual bool IsConflicted() const override;
virtual bool CanRevert() const override;
private:
EGitState::Type GetGitState() const;
public:
/** History of the item, if any */
TGitSourceControlHistory History;
/** Filename on disk */
FString LocalFilename;
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
/** Pending rev info with which a file must be resolved, invalid if no resolve pending */
FResolveInfo PendingResolveInfo;
UE_DEPRECATED(5.3, "Use PendingResolveInfo.BaseRevision instead")
#endif
/** File Id with which our local revision diverged from the remote revision */
FString PendingMergeBaseFileHash;
/** Status of the file */
FGitState State;
FGitSourceControlChangelist Changelist;
/** The timestamp of the last update */
FDateTime TimeStamp;
/** The action within the head branch TODO */
FString HeadAction;
/** The last file modification time in the head branch TODO */
int64 HeadModTime;
/** The change list the last modification TODO */
FString HeadCommit;
};

View File

@@ -0,0 +1,367 @@
// 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)
#pragma once
#include "GitSourceControlRevision.h"
#include "GitSourceControlState.h"
class FGitSourceControlState;
class FGitSourceControlCommand;
/**
* Helper struct for maintaining temporary files for passing to commands
*/
class FGitScopedTempFile
{
public:
/** Constructor - open & write string to temp file */
FGitScopedTempFile(const FText& InText);
/** Destructor - delete temp file */
~FGitScopedTempFile();
/** Get the filename of this temp file - empty if it failed to be created */
const FString& GetFilename() const;
private:
/** The filename we are writing to */
FString Filename;
};
struct FGitVersion;
class FGitLockedFilesCache
{
public:
static FDateTime LastUpdated;
static const TMap<FString, FString>& GetLockedFiles() { return LockedFiles; }
static void SetLockedFiles(const TMap<FString, FString>& newLocks);
static void AddLockedFile(const FString& filePath, const FString& lockUser);
static void RemoveLockedFile(const FString& filePath);
private:
static void OnFileLockChanged(const FString& filePath, const FString& lockUser, bool locked);
// update local read/write state when our own lock statuses change
static TMap<FString, FString> LockedFiles;
};
namespace GitSourceControlUtils
{
/**
* Returns an updated repo root if all selected files are in a plugin subfolder, and the plugin subfolder is a git repo
* This supports the case where each plugin is a sub module
*
* @param AbsoluteFilePaths The list of files in the SC operation
* @param PathToRepositoryRoot The original path to the repository root (used by default)
*/
FString ChangeRepositoryRootIfSubmodule(const TArray<FString>& AbsoluteFilePaths, const FString& PathToRepositoryRoot);
/**
* Returns an updated repo root if all selected file is in a plugin subfolder, and the plugin subfolder is a git repo
* This supports the case where each plugin is a sub module
*
* @param AbsoluteFilePath The file in the SC operation
* @param PathToRepositoryRoot The original path to the repository root (used by default)
*/
FString ChangeRepositoryRootIfSubmodule(const FString& AbsoluteFilePath, const FString& PathToRepositoryRoot);
/**
* Find the path to the Git binary, looking into a few places (standalone Git install, and other common tools embedding Git)
* @returns the path to the Git binary if found, or an empty string.
*/
FString FindGitBinaryPath();
/**
* Run a Git "version" command to check the availability of the binary.
* @param InPathToGitBinary The path to the Git binary
* @param OutGitVersion If provided, populate with the git version parsed from "version" command
* @returns true if the command succeeded and returned no errors
*/
bool CheckGitAvailability(const FString& InPathToGitBinary, FGitVersion* OutVersion = nullptr);
/**
* Parse the output from the "version" command into GitMajorVersion and GitMinorVersion.
* @param InVersionString The version string returned by `git --version`
* @param OutVersion The FGitVersion to populate
*/
void ParseGitVersion(const FString& InVersionString, FGitVersion* OutVersion);
/**
* Check git for various optional capabilities by various means.
* @param InPathToGitBinary The path to the Git binary
* @param OutGitVersion If provided, populate with the git version parsed from "version" command
*/
void FindGitCapabilities(const FString& InPathToGitBinary, FGitVersion* OutVersion);
/**
* Run a Git "lfs" command to check the availability of the "Large File System" extension.
* @param InPathToGitBinary The path to the Git binary
* @param OutGitVersion If provided, populate with the git version parsed from "version" command
*/
void FindGitLfsCapabilities(const FString& InPathToGitBinary, FGitVersion* OutVersion);
/**
* Find the root of the Git repository, looking from the provided path and upward in its parent directories
* @param InPath The path to the Game Directory (or any path or file in any git repository)
* @param OutRepositoryRoot The path to the root directory of the Git repository if found, else the path to the ProjectDir
* @returns true if the command succeeded and returned no errors
*/
bool FindRootDirectory(const FString& InPath, FString& OutRepositoryRoot);
/**
* Get Git config user.name & user.email
* @param InPathToGitBinary The path to the Git binary
* @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory (can be empty)
* @param OutUserName Name of the Git user configured for this repository (or globaly)
* @param OutEmailName E-mail of the Git user configured for this repository (or globaly)
*/
void GetUserConfig(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutUserName, FString& OutUserEmail);
/**
* Get Git current checked-out branch
* @param InPathToGitBinary The path to the Git binary
* @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory
* @param OutBranchName Name of the current checked-out branch (if any, ie. not in detached HEAD)
* @returns true if the command succeeded and returned no errors
*/
bool GetBranchName(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutBranchName);
/**
* Get Git remote tracking branch
* @returns false if the branch is not tracking a remote
*/
bool GetRemoteBranchName(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutBranchName);
/**
* Get Git remote tracking branches that match wildcard
* @returns false if no matching branches
*/
bool GetRemoteBranchesWildcard(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& PatternMatch, TArray<FString>& OutBranchNames);
/**
* Get Git current commit details
* @param InPathToGitBinary The path to the Git binary
* @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory
* @param OutCommitId Current Commit full SHA1
* @param OutCommitSummary Current Commit description's Summary
* @returns true if the command succeeded and returned no errors
*/
bool GetCommitInfo(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutCommitId, FString& OutCommitSummary);
/**
* Get the URL of the "origin" defaut remote server
* @param InPathToGitBinary The path to the Git binary
* @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory
* @param OutRemoteUrl URL of "origin" defaut remote server
* @returns true if the command succeeded and returned no errors
*/
bool GetRemoteUrl(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutRemoteUrl);
/**
* Run a Git command - output is a string TArray.
*
* @param InCommand The Git command - e.g. commit
* @param InPathToGitBinary The path to the Git binary
* @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory (can be empty)
* @param InParameters The parameters to the Git command
* @param InFiles The files to be operated on
* @param OutResults The results (from StdOut) as an array per-line
* @param OutErrorMessages Any errors (from StdErr) as an array per-line
* @returns true if the command succeeded and returned no errors
*/
bool RunCommand(const FString& InCommand, const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray<FString>& InParameters, const TArray<FString>& InFiles, TArray<FString>& OutResults, TArray<FString>& OutErrorMessages);
bool RunCommandInternalRaw(const FString& InCommand, const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray<FString>& InParameters, const TArray<FString>& InFiles, FString& OutResults, FString& OutErrors, const int32 ExpectedReturnCode = 0);
/**
* Unloads packages of specified named files
*/
TArray<class UPackage*> UnlinkPackages(const TArray<FString>& InPackageNames);
/**
* Reloads packages for these packages
*/
void ReloadPackages(TArray<UPackage*>& InPackagesToReload);
/**
* Gets all Git tracked files, including within directories, recursively
*/
bool ListFilesInDirectoryRecurse(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InDirectory, TArray<FString>& OutFiles);
/**
* Run a Git "commit" command by batches.
*
* @param InPathToGitBinary The path to the Git binary
* @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory
* @param InParameter The parameters to the Git commit command
* @param InFiles The files to be operated on
* @param OutErrorMessages Any errors (from StdErr) as an array per-line
* @returns true if the command succeeded and returned no errors
*/
bool RunCommit(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray<FString>& InParameters, const TArray<FString>& InFiles, TArray<FString>& OutResults, TArray<FString>& OutErrorMessages);
/**
* Checks remote branches to see file differences.
*
* @param CurrentBranchName The current branch we are on.
* @param InPathToGitBinary The path to the Git binary
* @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory
* @param OnePath The file to be checked
* @param OutErrorMessages Any errors (from StdErr) as an array per-line
*/
void CheckRemote(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray<FString>& Files,
TArray<FString>& OutErrorMessages, TMap<FString, FGitSourceControlState>& OutStates);
/**
* Run a Git "status" command and parse it.
*
* @param InPathToGitBinary The path to the Git binary
* @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory (can be empty)
* @param InUsingLfsLocking Tells if using the Git LFS file Locking workflow
* @param InFiles The files to be operated on
* @param OutErrorMessages Any errors (from StdErr) as an array per-line
* @param OutStates The resultant states
* @returns true if the command succeeded and returned no errors
*/
bool RunUpdateStatus(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool InUsingLfsLocking, const TArray<FString>& InFiles,
TArray<FString>& OutErrorMessages, TMap<FString, FGitSourceControlState>& OutStates);
/**
* Keep Consistency of being file staged
*
* @param Filename Saved filename
* @param Pkg Package (for adapting delegate)
* @param ObjectSaveContext Context for save (for adapting delegate)
*/
void UpdateFileStagingOnSaved(const FString& Filename, UPackage* Pkg, FObjectPostSaveContext ObjectSaveContext);
/**
* Keep Consistency of being file staged with simple argument
*
* @param Filename Saved filename
*/
bool UpdateFileStagingOnSavedInternal(const FString& Filename);
/**
*
*
* @param Filename Saved filename
* @param Pkg Package (for adapting delegate)
* @param ObjectSaveContext Context for save (for adapting delegate)
*/
void UpdateStateOnAssetRename(const FAssetData& InAssetData, const FString& InOldName);
/**
*
*
* @param Filename Saved filename
* @param Pkg Package (for adapting delegate)
* @param ObjectSaveContext Context for save (for adapting delegate)
*/
bool UpdateChangelistStateByCommand();
/**
* Run a Git "cat-file" command to dump the binary content of a revision into a file.
*
* @param InPathToGitBinary The path to the Git binary
* @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory
* @param InParameter The parameters to the Git show command (rev:path)
* @param InDumpFileName The temporary file to dump the revision
* @returns true if the command succeeded and returned no errors
*/
bool RunDumpToFile(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InParameter, const FString& InDumpFileName);
/**
* Run a Git "log" command and parse it.
*
* @param InPathToGitBinary The path to the Git binary
* @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory
* @param InFile The file to be operated on
* @param bMergeConflict In case of a merge conflict, we also need to get the tip of the "remote branch" (MERGE_HEAD) before the log of the "current branch" (HEAD)
* @param OutErrorMessages Any errors (from StdErr) as an array per-line
* @param OutHistory The history of the file
*/
bool RunGetHistory(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InFile, bool bMergeConflict, TArray<FString>& OutErrorMessages, TGitSourceControlHistory& OutHistory);
/**
* Helper function to convert a filename array to relative paths.
* @param InFileNames The filename array
* @param InRelativeTo Path to the WorkspaceRoot
* @return an array of filenames, transformed into relative paths
*/
TArray<FString> RelativeFilenames(const TArray<FString>& InFileNames, const FString& InRelativeTo);
/**
* Helper function to convert a filename array to absolute paths.
* @param InFileNames The filename array (relative paths)
* @param InRelativeTo Path to the WorkspaceRoot
* @return an array of filenames, transformed into absolute paths
*/
TArray<FString> AbsoluteFilenames(const TArray<FString>& InFileNames, const FString& InRelativeTo);
/**
* Remove redundant errors (that contain a particular string) and also
* update the commands success status if all errors were removed.
*/
void RemoveRedundantErrors(FGitSourceControlCommand& InCommand, const FString& InFilter);
bool RunLFSCommand(const FString& InCommand, const FString& InRepositoryRoot, const FString& GitBinaryFallback, const TArray<FString>& InParameters, const TArray<FString>& InFiles, TArray<FString>& OutResults, TArray<FString>& OutErrorMessages);
/**
* Helper function for various commands to update cached states.
* @returns true if any states were updated
*/
bool UpdateCachedStates(const TMap<const FString, FGitState>& InResults);
/**
* Helper function for various commands to collect new states.
* @returns true if any states were updated
*/
bool CollectNewStates(const TMap<FString, FGitSourceControlState>& InStates, TMap<const FString, FGitState>& OutResults);
/**
* Helper function for various commands to collect new states.
* @returns true if any states were updated
*/
bool CollectNewStates(const TArray<FString>& InFiles, TMap<const FString, FGitState>& OutResults, EFileState::Type FileState, ETreeState::Type TreeState = ETreeState::Unset, ELockState::Type LockState = ELockState::Unset, ERemoteState::Type RemoteState = ERemoteState::Unset);
/**
* Run 'git lfs locks" to extract all lock information for all files in the repository
*
* @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory
* @param GitBinaryFallBack The Git binary fallback path
* @param OutErrorMessages Any errors (from StdErr) as an array per-line
* @param OutLocks The lock results (file, username)
* @returns true if the command succeeded and returned no errors
*/
bool GetAllLocks(const FString& InRepositoryRoot, const FString& GitBinaryFallBack, TArray<FString>& OutErrorMessages, TMap<FString, FString>& OutLocks, bool bInvalidateCache = false);
/**
* Gets locks from state cache
*/
void GetLockedFiles(const TArray<FString>& InFiles, TArray<FString>& OutFiles);
/**
* Checks cache for if this file type is lockable
*/
bool IsFileLFSLockable(const FString& InFile);
/**
* Gets Git attribute to see if these extensions are lockable
*/
bool CheckLFSLockable(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray<FString>& InFiles, TArray<FString>& OutErrorMessages);
bool FetchRemote(const FString& InPathToGitBinary, const FString& InPathToRepositoryRoot, bool InUsingGitLfsLocking, TArray<FString>& OutResults, TArray<FString>& OutErrorMessages);
bool PullOrigin(const FString& InPathToGitBinary, const FString& InPathToRepositoryRoot, const TArray<FString>& InFiles, TArray<FString>& OutFiles,
TArray<FString>& OutResults, TArray<FString>& OutErrorMessages);
TSharedPtr< class ISourceControlRevision, ESPMode::ThreadSafe > GetOriginRevisionOnBranch( const FString & InPathToGitBinary, const FString & InRepositoryRoot, const FString & InRelativeFileName, TArray< FString > & OutErrorMessages, const FString & BranchName );
}

View File

@@ -0,0 +1,31 @@
// 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)
#pragma once
#include "Templates/SharedPointer.h"
class IGitSourceControlWorker
{
public:
/**
* Name describing the work that this worker does. Used for factory method hookup.
*/
virtual FName GetName() const = 0;
/**
* Function that actually does the work. Can be executed on another thread.
*/
virtual bool Execute( class FGitSourceControlCommand& InCommand ) = 0;
/**
* Updates the state of any items after completion (if necessary). This is always executed on the main thread.
* @returns true if states were updated
*/
virtual bool UpdateStates() const = 0;
};
typedef TSharedRef<IGitSourceControlWorker, ESPMode::ThreadSafe> FGitSourceControlWorkerRef;

View File

@@ -0,0 +1,982 @@
// 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 "SGitSourceControlSettings.h"
#include "Runtime/Launch/Resources/Version.h"
#include "Fonts/SlateFontInfo.h"
#include "Misc/App.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Modules/ModuleManager.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SEditableTextBox.h"
#include "Widgets/Input/SFilePathPicker.h"
#include "Widgets/Input/SMultiLineEditableTextBox.h"
#include "Widgets/Layout/SSeparator.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "Framework/Notifications/NotificationManager.h"
#include "EditorDirectories.h"
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
#else
#include "EditorStyleSet.h"
#endif
#include "SourceControlOperations.h"
#include "GitSourceControlModule.h"
#include "GitSourceControlUtils.h"
#define LOCTEXT_NAMESPACE "SGitSourceControlSettings"
void SGitSourceControlSettings::Construct(const FArguments& InArgs)
{
bAutoCreateGitIgnore = true;
bAutoCreateReadme = true;
bAutoCreateGitAttributes = false;
bAutoInitialCommit = true;
InitialCommitMessage = LOCTEXT("InitialCommitMessage", "Initial commit");
ReadmeContent = FText::FromString(FString(TEXT("# ")) + FApp::GetProjectName() + "\n\nDeveloped with Unreal Engine\n");
ConstructBasedOnEngineVersion( );
}
#if ENGINE_MAJOR_VERSION < 5
void SGitSourceControlSettings::ConstructBasedOnEngineVersion( )
{
const FText FileFilterType = NSLOCTEXT("GitSourceControl", "Executables", "Executables");
#if PLATFORM_WINDOWS
const FString FileFilterText = FString::Printf(TEXT("%s (*.exe)|*.exe"), *FileFilterType.ToString());
#else
const FString FileFilterText = FString::Printf(TEXT("%s"), *FileFilterType.ToString());
#endif
const FSlateFontInfo Font = FEditorStyle::GetFontStyle(TEXT("SourceControl.LoginWindow.Font"));
ChildSlot
[
SNew(SBorder)
.BorderImage( FEditorStyle::GetBrush("DetailsView.CategoryBottom"))
.Padding(FMargin(0.0f, 3.0f, 0.0f, 0.0f))
[
SNew(SVerticalBox)
// Path to the Git command line executable
+SVerticalBox::Slot()
.AutoHeight()
.Padding(2.0f)
.VAlign(VAlign_Center)
[
SNew(SHorizontalBox)
.ToolTipText(LOCTEXT("BinaryPathLabel_Tooltip", "Path to Git binary"))
+SHorizontalBox::Slot()
.FillWidth(1.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("BinaryPathLabel", "Git Path"))
.Font(Font)
]
+SHorizontalBox::Slot()
.FillWidth(2.0f)
[
SNew(SFilePathPicker)
.BrowseButtonImage(FEditorStyle::GetBrush("PropertyWindow.Button_Ellipsis"))
.BrowseButtonStyle(FEditorStyle::Get(), "HoverHintOnly")
.BrowseDirectory(FEditorDirectories::Get().GetLastDirectory(ELastDirectory::GENERIC_OPEN))
.BrowseTitle(LOCTEXT("BinaryPathBrowseTitle", "File picker..."))
.FilePath(this, &SGitSourceControlSettings::GetBinaryPathString)
.FileTypeFilter(FileFilterText)
.OnPathPicked(this, &SGitSourceControlSettings::OnBinaryPathPicked)
]
]
// Root of the local repository
+SVerticalBox::Slot()
.AutoHeight()
.Padding(2.0f)
.VAlign(VAlign_Center)
[
SNew(SHorizontalBox)
.ToolTipText(LOCTEXT("RepositoryRootLabel_Tooltip", "Path to the root of the Git repository"))
+SHorizontalBox::Slot()
.FillWidth(1.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("RepositoryRootLabel", "Root of the repository"))
.Font(Font)
]
+SHorizontalBox::Slot()
.FillWidth(2.0f)
[
SNew(STextBlock)
.Text(this, &SGitSourceControlSettings::GetPathToRepositoryRoot)
.Font(Font)
]
]
// User Name
+SVerticalBox::Slot()
.AutoHeight()
.Padding(2.0f)
.VAlign(VAlign_Center)
[
SNew(SHorizontalBox)
.ToolTipText(LOCTEXT("GitUserName_Tooltip", "User name configured for the Git repository"))
+SHorizontalBox::Slot()
.FillWidth(1.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("GitUserName", "User Name"))
.Font(Font)
]
+SHorizontalBox::Slot()
.FillWidth(2.0f)
[
SNew(STextBlock)
.Text(this, &SGitSourceControlSettings::GetUserName)
.Font(Font)
]
]
// User e-mail
+SVerticalBox::Slot()
.FillHeight(1.0f)
.Padding(2.0f)
.VAlign(VAlign_Center)
[
SNew(SHorizontalBox)
.ToolTipText(LOCTEXT("GitUserEmail_Tooltip", "User e-mail configured for the Git repository"))
+SHorizontalBox::Slot()
.FillWidth(1.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("GitUserEmail", "E-Mail"))
.Font(Font)
]
+SHorizontalBox::Slot()
.FillWidth(2.0f)
[
SNew(STextBlock)
.Text(this, &SGitSourceControlSettings::GetUserEmail)
.Font(Font)
]
]
// Separator
+SVerticalBox::Slot()
.AutoHeight()
.Padding(2.0f)
.VAlign(VAlign_Center)
[
SNew(SSeparator)
]
// Explanation text
+SVerticalBox::Slot()
.FillHeight(1.0f)
.Padding(2.0f)
.VAlign(VAlign_Center)
[
SNew(SHorizontalBox)
.Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository)
+SHorizontalBox::Slot()
.FillWidth(1.0f)
.HAlign(HAlign_Center)
[
SNew(STextBlock)
.Text(LOCTEXT("RepositoryNotFound", "Current Project is not contained in a Git Repository. Fill the form below to initialize a new Repository."))
.ToolTipText(LOCTEXT("RepositoryNotFound_Tooltip", "No Repository found at the level or above the current Project"))
.Font(Font)
]
]
// Option to configure the URL of the default remote 'origin'
// TODO: option to configure the name of the remote instead of the default origin
+SVerticalBox::Slot()
.AutoHeight()
.Padding(2.0f)
.VAlign(VAlign_Center)
[
SNew(SHorizontalBox)
.Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository)
.ToolTipText(LOCTEXT("ConfigureOrigin_Tooltip", "Configure the URL of the default remote 'origin'"))
+SHorizontalBox::Slot()
.FillWidth(1.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("ConfigureOrigin", "URL of the remote server 'origin'"))
.Font(Font)
]
+SHorizontalBox::Slot()
.FillWidth(2.0f)
.VAlign(VAlign_Center)
[
SNew(SEditableTextBox)
.Text(this, &SGitSourceControlSettings::GetRemoteUrl)
.OnTextCommitted(this, &SGitSourceControlSettings::OnRemoteUrlCommited)
.Font(Font)
]
]
// Option to add a proper .gitignore file (true by default)
+SVerticalBox::Slot()
.AutoHeight()
.Padding(2.0f)
.VAlign(VAlign_Center)
[
SNew(SHorizontalBox)
.Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository)
.ToolTipText(LOCTEXT("CreateGitIgnore_Tooltip", "Create and add a standard '.gitignore' file"))
+SHorizontalBox::Slot()
.FillWidth(0.1f)
[
SNew(SCheckBox)
.IsChecked(ECheckBoxState::Checked)
.OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedCreateGitIgnore)
]
+SHorizontalBox::Slot()
.FillWidth(2.9f)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(LOCTEXT("CreateGitIgnore", "Add a .gitignore file"))
.Font(Font)
]
]
// Option to add a README.md file with custom content
+SVerticalBox::Slot()
.AutoHeight()
.Padding(2.0f)
.VAlign(VAlign_Center)
[
SNew(SHorizontalBox)
.Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository)
.ToolTipText(LOCTEXT("CreateReadme_Tooltip", "Add a README.md file"))
+SHorizontalBox::Slot()
.FillWidth(0.1f)
[
SNew(SCheckBox)
.IsChecked(ECheckBoxState::Checked)
.OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedCreateReadme)
]
+SHorizontalBox::Slot()
.FillWidth(0.9f)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(LOCTEXT("CreateReadme", "Add a basic README.md file"))
.Font(Font)
]
+SHorizontalBox::Slot()
.FillWidth(2.0f)
.Padding(2.0f)
[
SNew(SMultiLineEditableTextBox)
.Text(this, &SGitSourceControlSettings::GetReadmeContent)
.OnTextCommitted(this, &SGitSourceControlSettings::OnReadmeContentCommited)
.IsEnabled(this, &SGitSourceControlSettings::GetAutoCreateReadme)
.SelectAllTextWhenFocused(true)
.Font(Font)
]
]
// Option to add a proper .gitattributes file for Git LFS (false by default)
+SVerticalBox::Slot()
.AutoHeight()
.Padding(2.0f)
.VAlign(VAlign_Center)
[
SNew(SHorizontalBox)
.Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository)
.ToolTipText(LOCTEXT("CreateGitAttributes_Tooltip", "Create and add a '.gitattributes' file to enable Git LFS for the whole 'Content/' directory (needs Git LFS extensions to be installed)."))
+SHorizontalBox::Slot()
.FillWidth(0.1f)
[
SNew(SCheckBox)
.IsChecked(ECheckBoxState::Unchecked)
.OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedCreateGitAttributes)
]
+SHorizontalBox::Slot()
.FillWidth(2.9f)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(LOCTEXT("CreateGitAttributes", "Add a .gitattributes file to enable Git LFS"))
.Font(Font)
]
]
// Option to use the Git LFS File Locking workflow (false by default)
// Enabled even after init to switch it off in case of no network
// TODO LFS turning it off afterwards does not work because all files are readonly !
+SVerticalBox::Slot()
.AutoHeight()
.Padding(2.0f)
.VAlign(VAlign_Center)
[
SNew(SHorizontalBox)
.ToolTipText(LOCTEXT("UseGitLfsLocking_Tooltip", "Uses Git LFS 2 File Locking workflow (CheckOut and Commit/Push)."))
+ SHorizontalBox::Slot()
.FillWidth(0.1f)
[
SNew(SCheckBox)
.IsChecked(SGitSourceControlSettings::IsUsingGitLfsLocking())
.OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedUseGitLfsLocking)
.IsEnabled(this, &SGitSourceControlSettings::CanUseGitLfsLocking)
]
+ SHorizontalBox::Slot()
.FillWidth(0.9f)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(LOCTEXT("UseGitLfsLocking", "Uses Git LFS 2 File Locking workflow"))
.Font(Font)
]
// Username credential used to access the Git LFS 2 File Locks server
+ SHorizontalBox::Slot()
.FillWidth(2.0f)
.VAlign(VAlign_Center)
[
SNew(SEditableTextBox)
.Text(this, &SGitSourceControlSettings::GetLfsUserName)
.OnTextCommitted(this, &SGitSourceControlSettings::OnLfsUserNameCommited)
.IsEnabled(this, &SGitSourceControlSettings::GetIsUsingGitLfsLocking)
.HintText(LOCTEXT("LfsUserName_Hint", "Username to lock files on the LFS server"))
.Font(Font)
]
]
// Option to Make the initial Git commit with custom message
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(2.0f)
.VAlign(VAlign_Center)
[
SNew(SHorizontalBox)
.Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository)
.ToolTipText(LOCTEXT("InitialGitCommit_Tooltip", "Make the initial Git commit"))
+ SHorizontalBox::Slot()
.FillWidth(0.1f)
[
SNew(SCheckBox)
.IsChecked(ECheckBoxState::Checked)
.OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedInitialCommit)
]
+ SHorizontalBox::Slot()
.FillWidth(0.9f)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(LOCTEXT("InitialGitCommit", "Make the initial Git commit"))
.Font(Font)
]
+ SHorizontalBox::Slot()
.FillWidth(2.0f)
.Padding(2.0f)
[
SNew(SMultiLineEditableTextBox)
.Text(this, &SGitSourceControlSettings::GetInitialCommitMessage)
.OnTextCommitted(this, &SGitSourceControlSettings::OnInitialCommitMessageCommited)
.IsEnabled(this, &SGitSourceControlSettings::GetAutoInitialCommit)
.SelectAllTextWhenFocused(true)
.Font(Font)
]
]
// Button to initialize the project with Git, create .gitignore/.gitattributes files, and make the first commit)
+ SVerticalBox::Slot()
.FillHeight(2.5f)
.Padding(4.0f)
.VAlign(VAlign_Center)
[
SNew(SHorizontalBox)
.Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository)
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
[
SNew(SButton)
.Text(LOCTEXT("GitInitRepository", "Initialize project with Git"))
.ToolTipText(LOCTEXT("GitInitRepository_Tooltip", "Initialize current project as a new Git repository"))
.OnClicked(this, &SGitSourceControlSettings::OnClickedInitializeGitRepository)
.IsEnabled(this, &SGitSourceControlSettings::CanInitializeGitRepository)
.HAlign(HAlign_Center)
.ContentPadding(6)
]
]
]
];
}
#else
void SGitSourceControlSettings::ConstructBasedOnEngineVersion( )
{
const FText FileFilterType = NSLOCTEXT("GitSourceControl", "Executables", "Executables");
#if PLATFORM_WINDOWS
const FString FileFilterText = FString::Printf(TEXT("%s (*.exe)|*.exe"), *FileFilterType.ToString());
#else
const FString FileFilterText = FString::Printf(TEXT("%s"), *FileFilterType.ToString());
#endif
using Self = std::remove_pointer_t<decltype(this)>;
#define ROW_LEFT( PADDING_HEIGHT ) +SHorizontalBox::Slot() \
.VAlign(VAlign_Center) \
.HAlign(HAlign_Right) \
.FillWidth(1.0f) \
.Padding(FMargin(0.0f, 0.0f, 16.0f, PADDING_HEIGHT))
#define ROW_RIGHT( PADDING_HEIGHT ) +SHorizontalBox::Slot() \
.VAlign(VAlign_Center) \
.FillWidth(2.0f) \
.Padding(FMargin(0.0f, 0.0f, 0.0f, PADDING_HEIGHT))
#define TT_GitPath LOCTEXT("BinaryPathLabel_Tooltip", "Path to Git binary")
#define TT_RepoRoot LOCTEXT("RepositoryRootLabel_Tooltip", "Path to the root of the Git repository")
#define TT_UserName LOCTEXT("UserNameLabel_Tooltip", "Git Username fetched from local config")
#define TT_Email LOCTEXT("GitUserEmail_Tooltip", "Git E-mail fetched from local config")
#define TT_LFS LOCTEXT("UseGitLfsLocking_Tooltip", "Uses Git LFS 2 File Locking workflow (CheckOut and Commit/Push).")
ChildSlot
[
SNew(SVerticalBox)
// Git Path
+SVerticalBox::Slot()
.AutoHeight()
[
SNew(SHorizontalBox)
ROW_LEFT( 10.0f )
[
SNew(STextBlock)
.Text(LOCTEXT("BinaryPathLabel", "Git Path"))
.ToolTipText( TT_GitPath )
]
ROW_RIGHT( 10.0f )
[
SNew(SFilePathPicker)
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
.BrowseButtonImage(FAppStyle::GetBrush("PropertyWindow.Button_Ellipsis"))
.BrowseButtonStyle(FAppStyle::Get(), "HoverHintOnly")
#else
.BrowseButtonImage(FEditorStyle::GetBrush("PropertyWindow.Button_Ellipsis"))
.BrowseButtonStyle(FEditorStyle::Get(), "HoverHintOnly")
#endif
.BrowseDirectory(FEditorDirectories::Get().GetLastDirectory(ELastDirectory::GENERIC_OPEN))
.BrowseTitle(LOCTEXT("BinaryPathBrowseTitle", "File picker..."))
.FilePath(this, &Self::GetBinaryPathString)
.FileTypeFilter(FileFilterText)
.OnPathPicked(this, &Self::OnBinaryPathPicked)
]
]
// Repository Root
+SVerticalBox::Slot()
[
SNew(SHorizontalBox)
ROW_LEFT( 10.0f )
[
SNew(STextBlock)
.Text(LOCTEXT("RepositoryRootLabel", "Root of the repository"))
.ToolTipText( TT_RepoRoot )
]
ROW_RIGHT( 10.0f )
[
SNew(STextBlock)
.Text(this, &Self::GetPathToRepositoryRoot)
.ToolTipText( TT_RepoRoot )
]
]
// User Name
+SVerticalBox::Slot()
[
SNew(SHorizontalBox)
ROW_LEFT( 10.0f )
[
SNew(STextBlock)
.Text(LOCTEXT("UserNameLabel", "User Name"))
.ToolTipText( TT_UserName )
]
ROW_RIGHT( 10.0f )
[
SNew(STextBlock)
.Text(this, &Self::GetUserName)
.ToolTipText( TT_UserName )
]
]
// Email
+SVerticalBox::Slot()
[
SNew(SHorizontalBox)
ROW_LEFT( 10.0f )
[
SNew(STextBlock)
.Text(LOCTEXT("EmailLabel", "E-mail"))
.ToolTipText( TT_Email )
]
ROW_RIGHT( 10.0f )
[
SNew(STextBlock)
.Text(this, &Self::GetUserEmail )
.ToolTipText( TT_Email )
]
]
// LFS Config
+SVerticalBox::Slot()
.AutoHeight()
[
SNew(SHorizontalBox)
ROW_LEFT( 10.0f )
[
SNew(SCheckBox)
.IsChecked(Self::IsUsingGitLfsLocking())
.OnCheckStateChanged(this, &Self::OnCheckedUseGitLfsLocking)
.IsEnabled(this, &Self::CanUseGitLfsLocking)
.Content()
[
SNew(STextBlock)
.Text(LOCTEXT("UseGitLfsLocking", "Uses Git LFS"))
.ToolTipText( TT_LFS )
]
]
ROW_RIGHT( 10.0f )
[
SNew(SEditableTextBox)
.Text(this, &Self::GetLfsUserName)
.OnTextCommitted(this, &Self::OnLfsUserNameCommited)
.IsEnabled(this, &Self::GetIsUsingGitLfsLocking)
.HintText(LOCTEXT("LfsUserName_Hint", "Username to lock files on the LFS server"))
]
]
// [Optional] Initial Git Commit
+SVerticalBox::Slot()
.AutoHeight()
.Padding(2.0f)
.VAlign(VAlign_Center)
[
SNew(SHorizontalBox)
.ToolTipText(LOCTEXT("InitialGitCommit_Tooltip", "Make the initial Git commit"))
.Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository)
+ SHorizontalBox::Slot()
.FillWidth(0.1f)
[
SNew(SCheckBox)
.IsChecked(ECheckBoxState::Checked)
.OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedInitialCommit)
]
+ SHorizontalBox::Slot()
.FillWidth(0.9f)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(LOCTEXT("InitialGitCommit", "Make the initial Git commit"))
]
+ SHorizontalBox::Slot()
.FillWidth(2.0f)
.Padding(2.0f)
[
SNew(SMultiLineEditableTextBox)
.Text(this, &SGitSourceControlSettings::GetInitialCommitMessage)
.OnTextCommitted(this, &SGitSourceControlSettings::OnInitialCommitMessageCommited)
.IsEnabled(this, &SGitSourceControlSettings::GetAutoInitialCommit)
.SelectAllTextWhenFocused(true)
]
]
// [Optional] Initialize Project with Git
+SVerticalBox::Slot()
.FillHeight(2.5f)
.Padding(4.0f)
.VAlign(VAlign_Center)
[
SNew(SHorizontalBox)
.Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository)
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
[
SNew(SButton)
.Text(LOCTEXT("GitInitRepository", "Initialize project with Git"))
.ToolTipText(LOCTEXT("GitInitRepository_Tooltip", "Initialize current project as a new Git repository"))
.OnClicked(this, &SGitSourceControlSettings::OnClickedInitializeGitRepository)
.IsEnabled(this, &SGitSourceControlSettings::CanInitializeGitRepository)
.HAlign(HAlign_Center)
.ContentPadding(6)
]
]
];
// TODO [RW] The UE5 GUI for the two optional initial git support functionalities has not been tested
}
#endif
SGitSourceControlSettings::~SGitSourceControlSettings()
{
RemoveInProgressNotification();
}
FString SGitSourceControlSettings::GetBinaryPathString() const
{
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
return GitSourceControl.AccessSettings().GetBinaryPath();
}
void SGitSourceControlSettings::OnBinaryPathPicked( const FString& PickedPath ) const
{
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
FString PickedFullPath = FPaths::ConvertRelativePathToFull(PickedPath);
const bool bChanged = GitSourceControl.AccessSettings().SetBinaryPath(PickedFullPath);
if(bChanged)
{
// Re-Check provided git binary path for each change
GitSourceControl.GetProvider().CheckGitAvailability();
if(GitSourceControl.GetProvider().IsGitAvailable())
{
GitSourceControl.SaveSettings();
}
}
}
FText SGitSourceControlSettings::GetPathToRepositoryRoot() const
{
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
const FString& PathToRepositoryRoot = GitSourceControl.GetProvider().GetPathToRepositoryRoot();
return FText::FromString(PathToRepositoryRoot);
}
FText SGitSourceControlSettings::GetUserName() const
{
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
const FString& UserName = GitSourceControl.GetProvider().GetUserName();
return FText::FromString(UserName);
}
FText SGitSourceControlSettings::GetUserEmail() const
{
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
const FString& UserEmail = GitSourceControl.GetProvider().GetUserEmail();
return FText::FromString(UserEmail);
}
EVisibility SGitSourceControlSettings::MustInitializeGitRepository() const
{
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
const bool bGitAvailable = GitSourceControl.GetProvider().IsGitAvailable();
const bool bGitRepositoryFound = GitSourceControl.GetProvider().IsEnabled();
#if 0
return (bGitAvailable && !bGitRepositoryFound) ? EVisibility::Visible : EVisibility::Collapsed;
#else
return EVisibility::Collapsed;
#endif
}
bool SGitSourceControlSettings::CanInitializeGitRepository() const
{
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
const bool bGitAvailable = GitSourceControl.GetProvider().IsGitAvailable();
const bool bGitRepositoryFound = GitSourceControl.GetProvider().IsEnabled();
const FString& LfsUserName = GitSourceControl.AccessSettings().GetLfsUserName();
const bool bIsUsingGitLfsLocking = GitSourceControl.GetProvider().UsesCheckout();
const bool bGitLfsConfigOk = !bIsUsingGitLfsLocking || !LfsUserName.IsEmpty();
const bool bInitialCommitConfigOk = !bAutoInitialCommit || !InitialCommitMessage.IsEmpty();
#if 0
return (bGitAvailable && !bGitRepositoryFound && bGitLfsConfigOk && bInitialCommitConfigOk);
#else
return false;
#endif
}
bool SGitSourceControlSettings::CanUseGitLfsLocking() const
{
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
// TODO LFS SRombauts : check if .gitattributes file is present and if Content/ is already tracked!
const bool bGitAttributesCreated = true;
return (bAutoCreateGitAttributes || bGitAttributesCreated);
}
FReply SGitSourceControlSettings::OnClickedInitializeGitRepository()
{
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
const FString PathToProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
TArray<FString> InfoMessages;
TArray<FString> ErrorMessages;
// 1.a. Synchronous (very quick) "git init" operation: initialize a Git local repository with a .git/ subdirectory
GitSourceControlUtils::RunCommand(TEXT("init"), PathToGitBinary, PathToProjectDir, FGitSourceControlModule::GetEmptyStringArray(), FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages);
// 1.b. Synchronous (very quick) "git remote add" operation: configure the URL of the default remote server 'origin' if specified
if(!RemoteUrl.IsEmpty())
{
TArray<FString> Parameters;
Parameters.Add(TEXT("add origin"));
Parameters.Add(RemoteUrl.ToString());
GitSourceControlUtils::RunCommand(TEXT("remote"), PathToGitBinary, PathToProjectDir, Parameters, FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages);
}
// Check the new repository status to enable connection (branch, user e-mail)
GitSourceControl.GetProvider().CheckGitAvailability();
if(GitSourceControl.GetProvider().IsAvailable())
{
// List of files to add to Revision Control (.uproject, Config/, Content/, Source/ files and .gitignore/.gitattributes if any)
TArray<FString> ProjectFiles;
ProjectFiles.Add(FPaths::ProjectContentDir());
ProjectFiles.Add(FPaths::ProjectConfigDir());
ProjectFiles.Add(FPaths::GetProjectFilePath());
if (FPaths::DirectoryExists(FPaths::GameSourceDir()))
{
ProjectFiles.Add(FPaths::GameSourceDir());
}
if(bAutoCreateGitIgnore)
{
// 2.a. Create a standard ".gitignore" file with common patterns for a typical Blueprint & C++ project
const FString GitIgnoreFilename = FPaths::Combine(FPaths::ProjectDir(), TEXT(".gitignore"));
const FString GitIgnoreContent = TEXT("Binaries\nDerivedDataCache\nIntermediate\nSaved\n.vscode\n.vs\n*.VC.db\n*.opensdf\n*.opendb\n*.sdf\n*.sln\n*.suo\n*.xcodeproj\n*.xcworkspace\n*.log");
if(FFileHelper::SaveStringToFile(GitIgnoreContent, *GitIgnoreFilename, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
ProjectFiles.Add(GitIgnoreFilename);
}
}
if(bAutoCreateReadme)
{
// 2.b. Create a "README.md" file with a custom description
const FString ReadmeFilename = FPaths::Combine(FPaths::ProjectDir(), TEXT("README.md"));
if (FFileHelper::SaveStringToFile(ReadmeContent.ToString(), *ReadmeFilename, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
ProjectFiles.Add(ReadmeFilename);
}
}
if(bAutoCreateGitAttributes)
{
// 2.c. Synchronous (very quick) "lfs install" operation: needs only to be run once by user
GitSourceControlUtils::RunCommand(TEXT("install"), PathToGitBinary, PathToProjectDir, FGitSourceControlModule::GetEmptyStringArray(), FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages);
// 2.d. Create a ".gitattributes" file to enable Git LFS (Large File System) for the whole "Content/" subdir
const FString GitAttributesFilename = FPaths::Combine(FPaths::ProjectDir(), TEXT(".gitattributes"));
FString GitAttributesContent;
if (GitSourceControl.GetProvider().UsesCheckout())
{
// Git LFS 2.x File Locking mechanism
GitAttributesContent = TEXT("Content/** filter=lfs diff=lfs merge=lfs -text lockable\n");
}
else
{
GitAttributesContent = TEXT("Content/** filter=lfs diff=lfs merge=lfs -text\n");
}
if(FFileHelper::SaveStringToFile(GitAttributesContent, *GitAttributesFilename, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
ProjectFiles.Add(GitAttributesFilename);
}
}
// 3. Add files to Revision Control: launch an asynchronous MarkForAdd operation
LaunchMarkForAddOperation(ProjectFiles);
// 4. The CheckIn will follow, at completion of the MarkForAdd operation
FGitSourceControlProvider& Provider = FGitSourceControlModule::Get().GetProvider();
Provider.CheckRepositoryStatus();
}
return FReply::Handled();
}
// Launch an asynchronous "MarkForAdd" operation and start an ongoing notification
void SGitSourceControlSettings::LaunchMarkForAddOperation(const TArray<FString>& InFiles)
{
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
TSharedRef<FMarkForAdd, ESPMode::ThreadSafe> MarkForAddOperation = ISourceControlOperation::Create<FMarkForAdd>();
#if ENGINE_MAJOR_VERSION >= 5
ECommandResult::Type Result = GitSourceControl.GetProvider().Execute(MarkForAddOperation, FSourceControlChangelistPtr(), InFiles, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateSP(this, &SGitSourceControlSettings::OnSourceControlOperationComplete));
#else
ECommandResult::Type Result = GitSourceControl.GetProvider().Execute(MarkForAddOperation, InFiles, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateSP(this, &SGitSourceControlSettings::OnSourceControlOperationComplete));
#endif
if (Result == ECommandResult::Succeeded)
{
DisplayInProgressNotification(MarkForAddOperation);
}
else
{
DisplayFailureNotification(MarkForAddOperation);
}
}
// Launch an asynchronous "CheckIn" operation and start another ongoing notification
void SGitSourceControlSettings::LaunchCheckInOperation()
{
TSharedRef<FCheckIn, ESPMode::ThreadSafe> CheckInOperation = ISourceControlOperation::Create<FCheckIn>();
CheckInOperation->SetDescription(InitialCommitMessage);
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
#if ENGINE_MAJOR_VERSION >= 5
ECommandResult::Type Result = GitSourceControl.GetProvider().Execute(CheckInOperation, FSourceControlChangelistPtr(), FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateSP(this, &SGitSourceControlSettings::OnSourceControlOperationComplete));
#else
ECommandResult::Type Result = GitSourceControl.GetProvider().Execute(CheckInOperation, FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateSP(this, &SGitSourceControlSettings::OnSourceControlOperationComplete));
#endif
if (Result == ECommandResult::Succeeded)
{
DisplayInProgressNotification(CheckInOperation);
}
else
{
DisplayFailureNotification(CheckInOperation);
}
}
/// Delegate called when a Revision control operation has completed: launch the next one and manage notifications
void SGitSourceControlSettings::OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult)
{
RemoveInProgressNotification();
// Report result with a notification
if (InResult == ECommandResult::Succeeded)
{
DisplaySuccessNotification(InOperation);
}
else
{
DisplayFailureNotification(InOperation);
}
if ((InOperation->GetName() == "MarkForAdd") && (InResult == ECommandResult::Succeeded) && bAutoInitialCommit)
{
// 4. optional initial Asynchronous commit with custom message: launch a "CheckIn" Operation
LaunchCheckInOperation();
}
}
// Display an ongoing notification during the whole operation
void SGitSourceControlSettings::DisplayInProgressNotification(const FSourceControlOperationRef& InOperation)
{
FNotificationInfo Info(InOperation->GetInProgressString());
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);
}
}
// Remove the ongoing notification at the end of the operation
void SGitSourceControlSettings::RemoveInProgressNotification()
{
if (OperationInProgressNotification.IsValid())
{
OperationInProgressNotification.Pin()->ExpireAndFadeout();
OperationInProgressNotification.Reset();
}
}
// Display a temporary success notification at the end of the operation
void SGitSourceControlSettings::DisplaySuccessNotification(const FSourceControlOperationRef& InOperation)
{
const FText NotificationText = FText::Format(LOCTEXT("InitialCommit_Success", "{0} operation was successfull!"), FText::FromName(InOperation->GetName()));
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);
}
// Display a temporary failure notification at the end of the operation
void SGitSourceControlSettings::DisplayFailureNotification(const FSourceControlOperationRef& InOperation)
{
const FText NotificationText = FText::Format(LOCTEXT("InitialCommit_Failure", "Error: {0} operation failed!"), FText::FromName(InOperation->GetName()));
FNotificationInfo Info(NotificationText);
Info.ExpireDuration = 8.0f;
FSlateNotificationManager::Get().AddNotification(Info);
}
void SGitSourceControlSettings::OnCheckedCreateGitIgnore(ECheckBoxState NewCheckedState)
{
bAutoCreateGitIgnore = (NewCheckedState == ECheckBoxState::Checked);
}
void SGitSourceControlSettings::OnCheckedCreateReadme(ECheckBoxState NewCheckedState)
{
bAutoCreateReadme = (NewCheckedState == ECheckBoxState::Checked);
}
bool SGitSourceControlSettings::GetAutoCreateReadme() const
{
return bAutoCreateReadme;
}
void SGitSourceControlSettings::OnReadmeContentCommited(const FText& InText, ETextCommit::Type InCommitType)
{
ReadmeContent = InText;
}
FText SGitSourceControlSettings::GetReadmeContent() const
{
return ReadmeContent;
}
void SGitSourceControlSettings::OnCheckedCreateGitAttributes(ECheckBoxState NewCheckedState)
{
bAutoCreateGitAttributes = (NewCheckedState == ECheckBoxState::Checked);
}
void SGitSourceControlSettings::OnCheckedUseGitLfsLocking(ECheckBoxState NewCheckedState)
{
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
GitSourceControl.AccessSettings().SetUsingGitLfsLocking(NewCheckedState == ECheckBoxState::Checked);
GitSourceControl.AccessSettings().SaveSettings();
GitSourceControl.GetProvider().UpdateSettings();
}
bool SGitSourceControlSettings::GetIsUsingGitLfsLocking() const
{
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
return GitSourceControl.AccessSettings().IsUsingGitLfsLocking();
}
ECheckBoxState SGitSourceControlSettings::IsUsingGitLfsLocking() const
{
return (GetIsUsingGitLfsLocking() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked);
}
void SGitSourceControlSettings::OnLfsUserNameCommited(const FText& InText, ETextCommit::Type InCommitType)
{
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
GitSourceControl.AccessSettings().SetLfsUserName(InText.ToString());
GitSourceControl.AccessSettings().SaveSettings();
GitSourceControl.GetProvider().UpdateSettings();
}
FText SGitSourceControlSettings::GetLfsUserName() const
{
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
const FString LFSUserName = GitSourceControl.AccessSettings().GetLfsUserName();
if (LFSUserName.IsEmpty())
{
const FText& UserName = GetUserName();
GitSourceControl.AccessSettings().SetLfsUserName(UserName.ToString());
GitSourceControl.AccessSettings().SaveSettings();
GitSourceControl.GetProvider().UpdateSettings();
return UserName;
}
else
{
return FText::FromString(LFSUserName);
}
}
void SGitSourceControlSettings::OnCheckedInitialCommit(ECheckBoxState NewCheckedState)
{
bAutoInitialCommit = (NewCheckedState == ECheckBoxState::Checked);
}
bool SGitSourceControlSettings::GetAutoInitialCommit() const
{
return bAutoInitialCommit;
}
void SGitSourceControlSettings::OnInitialCommitMessageCommited(const FText& InText, ETextCommit::Type InCommitType)
{
InitialCommitMessage = InText;
}
FText SGitSourceControlSettings::GetInitialCommitMessage() const
{
return InitialCommitMessage;
}
void SGitSourceControlSettings::OnRemoteUrlCommited(const FText& InText, ETextCommit::Type InCommitType)
{
RemoteUrl = InText;
}
FText SGitSourceControlSettings::GetRemoteUrl() const
{
return RemoteUrl;
}
#undef LOCTEXT_NAMESPACE

View File

@@ -0,0 +1,100 @@
// 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)
#pragma once
#include "Widgets/SCompoundWidget.h"
#include "ISourceControlProvider.h"
#include "Runtime/Launch/Resources/Version.h"
class SNotificationItem;
#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 2
namespace ETextCommit { enum Type : int; }
#else
namespace ETextCommit { enum Type; }
#endif
enum class ECheckBoxState : uint8;
class SGitSourceControlSettings : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SGitSourceControlSettings) {}
SLATE_END_ARGS()
public:
void Construct(const FArguments& InArgs);
~SGitSourceControlSettings();
private:
void ConstructBasedOnEngineVersion( );
/** Delegates to get Git binary path from/to settings */
FString GetBinaryPathString() const;
void OnBinaryPathPicked(const FString & PickedPath) const;
/** Delegate to get repository root, user name and email from provider */
FText GetPathToRepositoryRoot() const;
FText GetUserName() const;
FText GetUserEmail() const;
EVisibility MustInitializeGitRepository() const;
bool CanInitializeGitRepository() const;
bool CanUseGitLfsLocking() const;
/** Delegate to initialize a new Git repository */
FReply OnClickedInitializeGitRepository();
void OnCheckedCreateGitIgnore(ECheckBoxState NewCheckedState);
bool bAutoCreateGitIgnore;
/** Delegates to create a README.md file */
void OnCheckedCreateReadme(ECheckBoxState NewCheckedState);
bool GetAutoCreateReadme() const;
bool bAutoCreateReadme;
void OnReadmeContentCommited(const FText& InText, ETextCommit::Type InCommitType);
FText GetReadmeContent() const;
FText ReadmeContent;
void OnCheckedCreateGitAttributes(ECheckBoxState NewCheckedState);
bool bAutoCreateGitAttributes;
void OnCheckedUseGitLfsLocking(ECheckBoxState NewCheckedState);
ECheckBoxState IsUsingGitLfsLocking() const;
bool GetIsUsingGitLfsLocking() const;
void OnLfsUserNameCommited(const FText& InText, ETextCommit::Type InCommitType);
FText GetLfsUserName() const;
void OnCheckedInitialCommit(ECheckBoxState NewCheckedState);
bool GetAutoInitialCommit() const;
bool bAutoInitialCommit;
void OnInitialCommitMessageCommited(const FText& InText, ETextCommit::Type InCommitType);
FText GetInitialCommitMessage() const;
FText InitialCommitMessage;
void OnRemoteUrlCommited(const FText& InText, ETextCommit::Type InCommitType);
FText GetRemoteUrl() const;
FText RemoteUrl;
/** Launch initial asynchronous add and commit operations */
void LaunchMarkForAddOperation(const TArray<FString>& InFiles);
void LaunchCheckInOperation();
/** Delegate called when a revision control operation has completed */
void OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult);
/** Asynchronous operation progress notifications */
TWeakPtr<SNotificationItem> OperationInProgressNotification;
void DisplayInProgressNotification(const FSourceControlOperationRef& InOperation);
void RemoveInProgressNotification();
void DisplaySuccessNotification(const FSourceControlOperationRef& InOperation);
void DisplayFailureNotification(const FSourceControlOperationRef& InOperation);
};