2025-04-14 14:47:22 +03:00

910 lines
28 KiB
C++

// 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