text: // Copyright (c) 2014-2022 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
  