Init template repo for UE5
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
#include "GitSourceControlChangelist.h"
|
||||
|
||||
FGitSourceControlChangelist FGitSourceControlChangelist::WorkingChangelist(TEXT("Working"), true);
|
||||
FGitSourceControlChangelist FGitSourceControlChangelist::StagedChangelist(TEXT("Staged"), true);
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 );
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user