// 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 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("GitSourceControl"); bUsingGitLfsLocking = GitSourceControl.AccessSettings().IsUsingGitLfsLocking(); } // bForceConnection: not used anymore } void FGitSourceControlProvider::CheckGitAvailability() { FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("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 FGitSourceControlProvider::GetStateInternal(const FString& Filename) { TSharedRef* State = StateCache.Find(Filename); if(State != NULL) { // found cached item return (*State); } else { // cache an unknown state for this item TSharedRef 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& InFiles, TArray< TSharedRef >& OutState, EStateCacheUsage::Type InStateCacheUsage ) { if(!IsEnabled()) { return ECommandResult::Failed; } TArray AbsoluteFiles = SourceControlHelpers::AbsoluteFilenames(InFiles); if(InStateCacheUsage == EStateCacheUsage::ForceUpdate) { Execute(ISourceControlOperation::Create(), AbsoluteFiles); } for(const auto& AbsoluteFile : AbsoluteFiles) { OutState.Add(GetStateInternal(*AbsoluteFile)); } return ECommandResult::Succeeded; } TArray FGitSourceControlProvider::GetCachedStateByPredicate(TFunctionRef Predicate) const { TArray 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 FGitSourceControlProvider::GetFilesInCache() { TArray 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& InOperation, const TArray& 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 AbsoluteFiles = SourceControlHelpers::AbsoluteFilenames(InFiles); // Query to see if we allow this operation TSharedPtr 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& InOperation ) const { return false; } void FGitSourceControlProvider::CancelOperation( const TSharedRef& 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 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 > FGitSourceControlProvider::GetLabels( const FString& InMatchingSpec ) const { TArray< TSharedRef > 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 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