468 lines
14 KiB
C++
468 lines
14 KiB
C++
|
// 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 "GitSourceControlProvider.h"
|
||
|
|
||
|
#include "HAL/PlatformProcess.h"
|
||
|
#include "Misc/Paths.h"
|
||
|
#include "Misc/QueuedThreadPool.h"
|
||
|
#include "Modules/ModuleManager.h"
|
||
|
#include "Widgets/DeclarativeSyntaxSupport.h"
|
||
|
#include "GitSourceControlCommand.h"
|
||
|
#include "ISourceControlModule.h"
|
||
|
#include "GitSourceControlModule.h"
|
||
|
#include "GitSourceControlUtils.h"
|
||
|
#include "SGitSourceControlSettings.h"
|
||
|
#include "Logging/MessageLog.h"
|
||
|
#include "ScopedSourceControlProgress.h"
|
||
|
#include "SourceControlHelpers.h"
|
||
|
#include "SourceControlOperations.h"
|
||
|
#include "Interfaces/IPluginManager.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();
|
||
|
|
||
|
const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("GitSourceControl");
|
||
|
bUsingGitLfsLocking = GitSourceControl.AccessSettings().IsUsingGitLfsLocking();
|
||
|
}
|
||
|
|
||
|
// bForceConnection: not used anymore
|
||
|
}
|
||
|
|
||
|
void FGitSourceControlProvider::CheckGitAvailability()
|
||
|
{
|
||
|
FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("GitSourceControl");
|
||
|
FString 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 = GitSourceControlUtils::CheckGitAvailability(PathToGitBinary, &GitVersion);
|
||
|
if(bGitAvailable)
|
||
|
{
|
||
|
CheckRepositoryStatus(PathToGitBinary);
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
bGitAvailable = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void FGitSourceControlProvider::CheckRepositoryStatus(const FString& InPathToGitBinary)
|
||
|
{
|
||
|
// Find the path to the root Git directory (if any, else uses the ProjectDir)
|
||
|
const FString PathToProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
|
||
|
bGitRepositoryFound = GitSourceControlUtils::FindRootDirectory(PathToProjectDir, PathToRepositoryRoot);
|
||
|
if(bGitRepositoryFound)
|
||
|
{
|
||
|
GitSourceControlMenu.Register();
|
||
|
|
||
|
// Get branch name
|
||
|
bGitRepositoryFound = GitSourceControlUtils::GetBranchName(InPathToGitBinary, PathToRepositoryRoot, BranchName);
|
||
|
if(bGitRepositoryFound)
|
||
|
{
|
||
|
GitSourceControlUtils::GetRemoteUrl(InPathToGitBinary, PathToRepositoryRoot, RemoteUrl);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
UE_LOG(LogSourceControl, Error, TEXT("'%s' is not a valid Git repository"), *PathToRepositoryRoot);
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
UE_LOG(LogSourceControl, Warning, TEXT("'%s' is not part of a Git repository"), *FPaths::ProjectDir());
|
||
|
}
|
||
|
|
||
|
// Get user name & email (of the repository, else from the global Git config)
|
||
|
GitSourceControlUtils::GetUserConfig(InPathToGitBinary, PathToRepositoryRoot, UserName, UserEmail);
|
||
|
}
|
||
|
|
||
|
void FGitSourceControlProvider::Close()
|
||
|
{
|
||
|
// clear the cache
|
||
|
StateCache.Empty();
|
||
|
// Remove all extensions to the "Source Control" menu in the Editor Toolbar
|
||
|
GitSourceControlMenu.Unregister();
|
||
|
|
||
|
bGitAvailable = false;
|
||
|
bGitRepositoryFound = false;
|
||
|
UserName.Empty();
|
||
|
UserEmail.Empty();
|
||
|
}
|
||
|
|
||
|
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, bUsingGitLfsLocking) );
|
||
|
StateCache.Add(Filename, NewState);
|
||
|
return NewState;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
FText FGitSourceControlProvider::GetStatusText() const
|
||
|
{
|
||
|
FFormatNamedArguments Args;
|
||
|
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) );
|
||
|
|
||
|
return FText::Format( NSLOCTEXT("Status", "Provider: Git\nEnabledLabel", "Local repository: {RepositoryName}\nRemote origin: {RemoteUrl}\nUser: {UserName}\nE-mail: {UserEmail}\n[{BranchName} {CommitId}] {CommitSummary}"), Args );
|
||
|
}
|
||
|
|
||
|
/** Quick check if source control is enabled */
|
||
|
bool FGitSourceControlProvider::IsEnabled() const
|
||
|
{
|
||
|
return bGitRepositoryFound;
|
||
|
}
|
||
|
|
||
|
/** Quick check if source 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;
|
||
|
}
|
||
|
|
||
|
TArray<FString> AbsoluteFiles = SourceControlHelpers::AbsoluteFilenames(InFiles);
|
||
|
|
||
|
if(InStateCacheUsage == EStateCacheUsage::ForceUpdate)
|
||
|
{
|
||
|
Execute(ISourceControlOperation::Create<FUpdateStatus>(), AbsoluteFiles);
|
||
|
}
|
||
|
|
||
|
for(const auto& AbsoluteFile : AbsoluteFiles)
|
||
|
{
|
||
|
OutState.Add(GetStateInternal(*AbsoluteFile));
|
||
|
}
|
||
|
|
||
|
return ECommandResult::Succeeded;
|
||
|
}
|
||
|
|
||
|
TArray<FSourceControlStateRef> FGitSourceControlProvider::GetCachedStateByPredicate(TFunctionRef<bool(const FSourceControlStateRef&)> Predicate) const
|
||
|
{
|
||
|
TArray<FSourceControlStateRef> Result;
|
||
|
for(const auto& CacheItem : StateCache)
|
||
|
{
|
||
|
FSourceControlStateRef State = CacheItem.Value;
|
||
|
if(Predicate(State))
|
||
|
{
|
||
|
Result.Add(State);
|
||
|
}
|
||
|
}
|
||
|
return Result;
|
||
|
}
|
||
|
|
||
|
bool FGitSourceControlProvider::RemoveFileFromCache(const FString& Filename)
|
||
|
{
|
||
|
return StateCache.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 );
|
||
|
}
|
||
|
|
||
|
ECommandResult::Type FGitSourceControlProvider::Execute( const TSharedRef<ISourceControlOperation, ESPMode::ThreadSafe>& InOperation, const TArray<FString>& InFiles, EConcurrency::Type InConcurrency, const FSourceControlOperationComplete& InOperationCompleteDelegate )
|
||
|
{
|
||
|
if(!IsEnabled() && !(InOperation->GetName() == "Connect")) // Only Connect operation allowed while not Enabled (Repository found)
|
||
|
{
|
||
|
InOperationCompleteDelegate.ExecuteIfBound(InOperation, ECommandResult::Failed);
|
||
|
return ECommandResult::Failed;
|
||
|
}
|
||
|
|
||
|
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 source 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 source control provider '{ProviderName}'"), Arguments));
|
||
|
FMessageLog("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->OperationCompleteDelegate = InOperationCompleteDelegate;
|
||
|
|
||
|
// fire off operation
|
||
|
if(InConcurrency == EConcurrency::Synchronous)
|
||
|
{
|
||
|
Command->bAutoDelete = false;
|
||
|
|
||
|
UE_LOG(LogSourceControl, Log, TEXT("ExecuteSynchronousCommand(%s)"), *InOperation->GetName().ToString());
|
||
|
return ExecuteSynchronousCommand(*Command, InOperation->GetInProgressString());
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Command->bAutoDelete = true;
|
||
|
|
||
|
UE_LOG(LogSourceControl, Log, TEXT("IssueAsynchronousCommand(%s)"), *InOperation->GetName().ToString());
|
||
|
return IssueCommand(*Command);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool FGitSourceControlProvider::CanCancelOperation( const TSharedRef<ISourceControlOperation, ESPMode::ThreadSafe>& InOperation ) const
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
void FGitSourceControlProvider::CancelOperation( const TSharedRef<ISourceControlOperation, ESPMode::ThreadSafe>& InOperation )
|
||
|
{
|
||
|
}
|
||
|
|
||
|
bool FGitSourceControlProvider::UsesLocalReadOnlyState() const
|
||
|
{
|
||
|
return bUsingGitLfsLocking; // Git LFS Lock uses read-only state
|
||
|
}
|
||
|
|
||
|
bool FGitSourceControlProvider::UsesChangelists() const
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
bool FGitSourceControlProvider::UsesCheckout() const
|
||
|
{
|
||
|
return bUsingGitLfsLocking; // Git LFS Lock uses read-only state
|
||
|
}
|
||
|
|
||
|
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
|
||
|
{
|
||
|
FMessageLog SourceControlLog("SourceControl");
|
||
|
|
||
|
for(int32 ErrorIndex = 0; ErrorIndex < InCommand.ErrorMessages.Num(); ++ErrorIndex)
|
||
|
{
|
||
|
SourceControlLog.Error(FText::FromString(InCommand.ErrorMessages[ErrorIndex]));
|
||
|
}
|
||
|
|
||
|
for(int32 InfoIndex = 0; InfoIndex < InCommand.InfoMessages.Num(); ++InfoIndex)
|
||
|
{
|
||
|
SourceControlLog.Info(FText::FromString(InCommand.InfoMessages[InfoIndex]));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void FGitSourceControlProvider::UpdateRepositoryStatus(const class FGitSourceControlCommand& InCommand)
|
||
|
{
|
||
|
// For all operations running UpdateStatus, get Commit informations:
|
||
|
if (!InCommand.CommitId.IsEmpty())
|
||
|
{
|
||
|
CommitId = InCommand.CommitId;
|
||
|
CommitSummary = InCommand.CommitSummary;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void FGitSourceControlProvider::Tick()
|
||
|
{
|
||
|
bool bStatesUpdated = false;
|
||
|
|
||
|
for(int32 CommandIndex = 0; CommandIndex < CommandQueue.Num(); ++CommandIndex)
|
||
|
{
|
||
|
FGitSourceControlCommand& Command = *CommandQueue[CommandIndex];
|
||
|
if(Command.bExecuteProcessed)
|
||
|
{
|
||
|
// Remove command from the queue
|
||
|
CommandQueue.RemoveAt(CommandIndex);
|
||
|
|
||
|
// Update respository 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
|
||
|
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;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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 SOURCE_CONTROL_WITH_SLATE
|
||
|
TSharedRef<class SWidget> FGitSourceControlProvider::MakeSettingsWidget() const
|
||
|
{
|
||
|
return SNew(SGitSourceControlSettings);
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
ECommandResult::Type FGitSourceControlProvider::ExecuteSynchronousCommand(FGitSourceControlCommand& InCommand, const FText& Task)
|
||
|
{
|
||
|
ECommandResult::Type Result = ECommandResult::Failed;
|
||
|
|
||
|
// Display the progress dialog if a string was provided
|
||
|
{
|
||
|
FScopedSourceControlProgress Progress(Task);
|
||
|
|
||
|
// Issue the command asynchronously...
|
||
|
IssueCommand( InCommand );
|
||
|
|
||
|
// ... then wait for its completion (thus making it synchronous)
|
||
|
while(!InCommand.bExecuteProcessed)
|
||
|
{
|
||
|
// Tick the command queue and update progress.
|
||
|
Tick();
|
||
|
|
||
|
Progress.Tick();
|
||
|
|
||
|
// Sleep so we don't busy-wait so much.
|
||
|
FPlatformProcess::Sleep(0.01f);
|
||
|
}
|
||
|
|
||
|
// always do one more Tick() to make sure the command queue is cleaned up.
|
||
|
Tick();
|
||
|
|
||
|
if(InCommand.bCommandSuccessful)
|
||
|
{
|
||
|
Result = ECommandResult::Succeeded;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Delete the command now (asynchronous commands are deleted in the Tick() method)
|
||
|
check(!InCommand.bAutoDelete);
|
||
|
|
||
|
// ensure commands that are not auto deleted do not end up in the command queue
|
||
|
if ( CommandQueue.Contains( &InCommand ) )
|
||
|
{
|
||
|
CommandQueue.Remove( &InCommand );
|
||
|
}
|
||
|
delete &InCommand;
|
||
|
|
||
|
return Result;
|
||
|
}
|
||
|
|
||
|
ECommandResult::Type FGitSourceControlProvider::IssueCommand(FGitSourceControlCommand& InCommand)
|
||
|
{
|
||
|
if(GThreadPool != nullptr)
|
||
|
{
|
||
|
// Queue this to our worker thread(s) for resolving
|
||
|
GThreadPool->AddQueuedWork(&InCommand);
|
||
|
CommandQueue.Add(&InCommand);
|
||
|
return ECommandResult::Succeeded;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
FText Message(LOCTEXT("NoSCCThreads", "There are no threads available to process the source control command."));
|
||
|
|
||
|
FMessageLog("SourceControl").Error(Message);
|
||
|
InCommand.bCommandSuccessful = false;
|
||
|
InCommand.Operation->AddErrorMessge(Message);
|
||
|
|
||
|
return InCommand.ReturnResults();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#undef LOCTEXT_NAMESPACE
|