From 56b745640525efe9810de4c7da527a8cd6ae8164 Mon Sep 17 00:00:00 2001 From: Stephen Whittle Date: Fri, 7 Feb 2025 15:45:41 +1100 Subject: [PATCH 01/11] Stub out concrete implementation of LFS lock provider --- .../Private/GitSourceControlSettings.cpp | 9 ++++++++ .../Private/LFSLockProvider.cpp | 2 ++ .../Private/LFSLockProvider.h | 11 ++++++++++ .../Public/GitSourceControlSettings.h | 6 ++++++ Source/GitSourceControl/Public/IGitLockInfo.h | 7 +++++++ .../Public/IGitLockProvider.h | 21 +++++++++++++++++++ 6 files changed, 56 insertions(+) create mode 100644 Source/GitSourceControl/Private/LFSLockProvider.cpp create mode 100644 Source/GitSourceControl/Private/LFSLockProvider.h create mode 100644 Source/GitSourceControl/Public/IGitLockInfo.h create mode 100644 Source/GitSourceControl/Public/IGitLockProvider.h diff --git a/Source/GitSourceControl/Private/GitSourceControlSettings.cpp b/Source/GitSourceControl/Private/GitSourceControlSettings.cpp index 3d28e15a..247f0322 100644 --- a/Source/GitSourceControl/Private/GitSourceControlSettings.cpp +++ b/Source/GitSourceControl/Private/GitSourceControlSettings.cpp @@ -74,6 +74,14 @@ void FGitSourceControlSettings::LoadSettings() GConfig->GetString(*GitSettingsConstants::SettingsSection, TEXT("BinaryPath"), BinaryPath, IniFile); GConfig->GetBool(*GitSettingsConstants::SettingsSection, TEXT("UsingGitLfsLocking"), bUsingGitLfsLocking, IniFile); GConfig->GetString(*GitSettingsConstants::SettingsSection, TEXT("LfsUserName"), LfsUserName, IniFile); + FString LockProviderClassPath; + GConfig->GetString(*GitSettingsConstants::SettingsSection, TEXT("LockProviderClass"), LockProviderClassPath, IniFile); + if (LockProviderClassPath.IsEmpty()) + { + LockProviderClassPath = TEXT("/Game/Blah/DefaultLockProvider"); + } + LockProviderClass = TSoftClassPtr {LockProviderClassPath}; + } void FGitSourceControlSettings::SaveSettings() const @@ -83,4 +91,5 @@ void FGitSourceControlSettings::SaveSettings() const GConfig->SetString(*GitSettingsConstants::SettingsSection, TEXT("BinaryPath"), *BinaryPath, IniFile); GConfig->SetBool(*GitSettingsConstants::SettingsSection, TEXT("UsingGitLfsLocking"), bUsingGitLfsLocking, IniFile); GConfig->SetString(*GitSettingsConstants::SettingsSection, TEXT("LfsUserName"), *LfsUserName, IniFile); + GConfig->SetString(*GitSettingsConstants::SettingsSection, TEXT("LockProviderClass"), LockProviderClass.ToString(), IniFile); } diff --git a/Source/GitSourceControl/Private/LFSLockProvider.cpp b/Source/GitSourceControl/Private/LFSLockProvider.cpp new file mode 100644 index 00000000..20f7c61f --- /dev/null +++ b/Source/GitSourceControl/Private/LFSLockProvider.cpp @@ -0,0 +1,2 @@ +#include "LFSLockProvider.h" + diff --git a/Source/GitSourceControl/Private/LFSLockProvider.h b/Source/GitSourceControl/Private/LFSLockProvider.h new file mode 100644 index 00000000..9a0a15af --- /dev/null +++ b/Source/GitSourceControl/Private/LFSLockProvider.h @@ -0,0 +1,11 @@ +#pragma once + +#include "IGitLogProvider.h" + +UCLASS() +class ULFSLockProvider : public UGitLockProviderBase +{ + public: + virtual bool RunLFSCommand(const FString& InCommand, const FString& InRepositoryRoot, const FString& GitBinaryFallback, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages) override; + +}; diff --git a/Source/GitSourceControl/Public/GitSourceControlSettings.h b/Source/GitSourceControl/Public/GitSourceControlSettings.h index 75718b9a..3c1a3576 100644 --- a/Source/GitSourceControl/Public/GitSourceControlSettings.h +++ b/Source/GitSourceControl/Public/GitSourceControlSettings.h @@ -29,6 +29,10 @@ class GITSOURCECONTROL_API FGitSourceControlSettings /** Set the username used by the Git LFS 2 File Locks server */ bool SetLfsUserName(const FString& InString); + const TSoftClassPtr GetLockProviderClass() const; + + bool SetLockProviderClass(TSoftClassPtr Provider); + /** Load settings from ini file */ void LoadSettings(); @@ -47,4 +51,6 @@ class GITSOURCECONTROL_API FGitSourceControlSettings /** Username used by the Git LFS 2 File Locks server */ FString LfsUserName; + + TSoftClassPtr LockProviderClass; }; diff --git a/Source/GitSourceControl/Public/IGitLockInfo.h b/Source/GitSourceControl/Public/IGitLockInfo.h new file mode 100644 index 00000000..8d17f26c --- /dev/null +++ b/Source/GitSourceControl/Public/IGitLockInfo.h @@ -0,0 +1,7 @@ +#pragma once + +class IGitLockInfo +{ + virtual FString GetLockOwner() = 0; + virtual FString GetLockPath() = 0; +}; diff --git a/Source/GitSourceControl/Public/IGitLockProvider.h b/Source/GitSourceControl/Public/IGitLockProvider.h new file mode 100644 index 00000000..05d068e4 --- /dev/null +++ b/Source/GitSourceControl/Public/IGitLockProvider.h @@ -0,0 +1,21 @@ +#pragma once + + +UINTERFACE(NotImplementableInBlueprint) +class UGitLockProvider +{ + GENERATED_BODY() +}; + +class IGitLockProvider +{ + public: + virtual bool RunLFSCommand(const FString& InCommand, const FString& InRepositoryRoot, const FString& GitBinaryFallback, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages) = 0; + +}; + +UCLASS() +class UGitLockProviderBase : public UObject, public IGitLockProvider +{ + GENERATED_BODY() +}; From 574c84caebf70fb2589076a412eceee608a199d7 Mon Sep 17 00:00:00 2001 From: Stephen Whittle Date: Mon, 10 Feb 2025 10:04:43 +1100 Subject: [PATCH 02/11] Finish stubbing, now compiles ok --- .../Private/GitSourceControlSettings.cpp | 17 ++++++++------- .../Private/LFSLockProvider.cpp | 7 +++++++ .../Private/LFSLockProvider.h | 13 ++++++++---- .../Public/IGitLockProvider.h | 21 ++++++++++++++----- 4 files changed, 41 insertions(+), 17 deletions(-) diff --git a/Source/GitSourceControl/Private/GitSourceControlSettings.cpp b/Source/GitSourceControl/Private/GitSourceControlSettings.cpp index 247f0322..45491747 100644 --- a/Source/GitSourceControl/Private/GitSourceControlSettings.cpp +++ b/Source/GitSourceControl/Private/GitSourceControlSettings.cpp @@ -11,12 +11,12 @@ namespace GitSettingsConstants { -/** The section of the ini file we load our settings from */ -static const FString SettingsSection = TEXT("GitSourceControl.GitSourceControlSettings"); + /** The section of the ini file we load our settings from */ + static const FString SettingsSection = TEXT("GitSourceControl.GitSourceControlSettings"); -} +} // namespace GitSettingsConstants -const FString & FGitSourceControlSettings::GetBinaryPath() const +const FString& FGitSourceControlSettings::GetBinaryPath() const { FScopeLock ScopeLock(&CriticalSection); return BinaryPath; // Return a copy to be thread-safe @@ -26,7 +26,7 @@ bool FGitSourceControlSettings::SetBinaryPath(const FString& InString) { FScopeLock ScopeLock(&CriticalSection); const bool bChanged = (BinaryPath != InString); - if(bChanged) + if (bChanged) { BinaryPath = InString; } @@ -75,13 +75,13 @@ void FGitSourceControlSettings::LoadSettings() GConfig->GetBool(*GitSettingsConstants::SettingsSection, TEXT("UsingGitLfsLocking"), bUsingGitLfsLocking, IniFile); GConfig->GetString(*GitSettingsConstants::SettingsSection, TEXT("LfsUserName"), LfsUserName, IniFile); FString LockProviderClassPath; - GConfig->GetString(*GitSettingsConstants::SettingsSection, TEXT("LockProviderClass"), LockProviderClassPath, IniFile); + GConfig->GetString(*GitSettingsConstants::SettingsSection, TEXT("LockProviderClass"), LockProviderClassPath, + IniFile); if (LockProviderClassPath.IsEmpty()) { LockProviderClassPath = TEXT("/Game/Blah/DefaultLockProvider"); } LockProviderClass = TSoftClassPtr {LockProviderClassPath}; - } void FGitSourceControlSettings::SaveSettings() const @@ -91,5 +91,6 @@ void FGitSourceControlSettings::SaveSettings() const GConfig->SetString(*GitSettingsConstants::SettingsSection, TEXT("BinaryPath"), *BinaryPath, IniFile); GConfig->SetBool(*GitSettingsConstants::SettingsSection, TEXT("UsingGitLfsLocking"), bUsingGitLfsLocking, IniFile); GConfig->SetString(*GitSettingsConstants::SettingsSection, TEXT("LfsUserName"), *LfsUserName, IniFile); - GConfig->SetString(*GitSettingsConstants::SettingsSection, TEXT("LockProviderClass"), LockProviderClass.ToString(), IniFile); + GConfig->SetString(*GitSettingsConstants::SettingsSection, TEXT("LockProviderClass"), *LockProviderClass.ToString(), + IniFile); } diff --git a/Source/GitSourceControl/Private/LFSLockProvider.cpp b/Source/GitSourceControl/Private/LFSLockProvider.cpp index 20f7c61f..b6d14816 100644 --- a/Source/GitSourceControl/Private/LFSLockProvider.cpp +++ b/Source/GitSourceControl/Private/LFSLockProvider.cpp @@ -1,2 +1,9 @@ #include "LFSLockProvider.h" +bool ULFSLockProvider::RunLFSCommand(const FString& InCommand, const FString& InRepositoryRoot, + const FString& GitBinaryFallback, const TArray& InParameters, + const TArray& InFiles, TArray& OutResults, + TArray& OutErrorMessages) +{ + return false; +} diff --git a/Source/GitSourceControl/Private/LFSLockProvider.h b/Source/GitSourceControl/Private/LFSLockProvider.h index 9a0a15af..30a6df82 100644 --- a/Source/GitSourceControl/Private/LFSLockProvider.h +++ b/Source/GitSourceControl/Private/LFSLockProvider.h @@ -1,11 +1,16 @@ #pragma once -#include "IGitLogProvider.h" +#include "IGitLockProvider.h" + +#include "LFSLockProvider.generated.h" UCLASS() class ULFSLockProvider : public UGitLockProviderBase { - public: - virtual bool RunLFSCommand(const FString& InCommand, const FString& InRepositoryRoot, const FString& GitBinaryFallback, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages) override; - + GENERATED_BODY() +public: + virtual bool RunLFSCommand(const FString& InCommand, const FString& InRepositoryRoot, + const FString& GitBinaryFallback, const TArray& InParameters, + const TArray& InFiles, TArray& OutResults, + TArray& OutErrorMessages) override; }; diff --git a/Source/GitSourceControl/Public/IGitLockProvider.h b/Source/GitSourceControl/Public/IGitLockProvider.h index 05d068e4..1fe39623 100644 --- a/Source/GitSourceControl/Public/IGitLockProvider.h +++ b/Source/GitSourceControl/Public/IGitLockProvider.h @@ -1,21 +1,32 @@ #pragma once +#include "IGitLockProvider.generated.h" -UINTERFACE(NotImplementableInBlueprint) -class UGitLockProvider +UINTERFACE(meta = (CannotImplementInterfaceInBlueprint)) +class UGitLockProvider : public UInterface { GENERATED_BODY() }; class IGitLockProvider { - public: - virtual bool RunLFSCommand(const FString& InCommand, const FString& InRepositoryRoot, const FString& GitBinaryFallback, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages) = 0; - + GENERATED_BODY() +public: + virtual bool RunLFSCommand(const FString& InCommand, const FString& InRepositoryRoot, + const FString& GitBinaryFallback, const TArray& InParameters, + const TArray& InFiles, TArray& OutResults, + TArray& OutErrorMessages) = 0; }; UCLASS() class UGitLockProviderBase : public UObject, public IGitLockProvider { GENERATED_BODY() +public: + bool RunLFSCommand(const FString& InCommand, const FString& InRepositoryRoot, const FString& GitBinaryFallback, + const TArray& InParameters, const TArray& InFiles, TArray& OutResults, + TArray& OutErrorMessages) override + { + return false; + } }; From 0d264f0e6a104afddea1fa0fb09bae09f4526955 Mon Sep 17 00:00:00 2001 From: Stephen Whittle Date: Mon, 10 Feb 2025 10:39:05 +1100 Subject: [PATCH 03/11] Set/get lock provider from source control settings --- .../GitSourceControl.Build.cs | 3 +- .../Private/GitSourceControlSettings.cpp | 11 ++ .../Private/SGitSourceControlSettings.cpp | 140 +++++++++++++----- .../Private/SGitSourceControlSettings.h | 25 ++-- 4 files changed, 132 insertions(+), 47 deletions(-) diff --git a/Source/GitSourceControl/GitSourceControl.Build.cs b/Source/GitSourceControl/GitSourceControl.Build.cs index fa732ae5..5b58ca1c 100644 --- a/Source/GitSourceControl/GitSourceControl.Build.cs +++ b/Source/GitSourceControl/GitSourceControl.Build.cs @@ -21,7 +21,8 @@ public GitSourceControl(ReadOnlyTargetRules Target) : base(Target) "UnrealEd", "SourceControl", "SourceControlWindows", - "Projects" + "Projects", + "PropertyEditor" } ); diff --git a/Source/GitSourceControl/Private/GitSourceControlSettings.cpp b/Source/GitSourceControl/Private/GitSourceControlSettings.cpp index 45491747..a5680795 100644 --- a/Source/GitSourceControl/Private/GitSourceControlSettings.cpp +++ b/Source/GitSourceControl/Private/GitSourceControlSettings.cpp @@ -66,6 +66,17 @@ bool FGitSourceControlSettings::SetLfsUserName(const FString& InString) return bChanged; } +const TSoftClassPtr FGitSourceControlSettings::GetLockProviderClass() const +{ + return LockProviderClass; +} + +bool FGitSourceControlSettings::SetLockProviderClass(TSoftClassPtr Provider) +{ + LockProviderClass = Provider; + return true; +} + // This is called at startup nearly before anything else in our module: BinaryPath will then be used by the provider void FGitSourceControlSettings::LoadSettings() { diff --git a/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp b/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp index a1c3f7b5..fc4d150d 100644 --- a/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp +++ b/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp @@ -5,30 +5,32 @@ #include "SGitSourceControlSettings.h" -#include "Runtime/Launch/Resources/Version.h" +#include "EditorDirectories.h" #include "Fonts/SlateFontInfo.h" +#include "Framework/Notifications/NotificationManager.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 "Runtime/Launch/Resources/Version.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" +#include "Widgets/SBoxPanel.h" +#include "Widgets/Text/STextBlock.h" #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 #else -#include "EditorStyleSet.h" + #include "EditorStyleSet.h" #endif -#include "SourceControlOperations.h" #include "GitSourceControlModule.h" #include "GitSourceControlUtils.h" - +#include "IGitLockProvider.h" +#include "LFSLockProvider.h" +#include "PropertyCustomizationHelpers.h" +#include "SourceControlOperations.h" #define LOCTEXT_NAMESPACE "SGitSourceControlSettings" @@ -40,11 +42,12 @@ void SGitSourceControlSettings::Construct(const FArguments& InArgs) bAutoInitialCommit = true; InitialCommitMessage = LOCTEXT("InitialCommitMessage", "Initial commit"); - ReadmeContent = FText::FromString(FString(TEXT("# ")) + FApp::GetProjectName() + "\n\nDeveloped with Unreal Engine\n"); + ReadmeContent = + FText::FromString(FString(TEXT("# ")) + FApp::GetProjectName() + "\n\nDeveloped with Unreal Engine\n"); - ConstructBasedOnEngineVersion( ); + ConstructBasedOnEngineVersion(); } - +// clang-format off #if ENGINE_MAJOR_VERSION < 5 void SGitSourceControlSettings::ConstructBasedOnEngineVersion( ) { @@ -534,6 +537,26 @@ void SGitSourceControlSettings::ConstructBasedOnEngineVersion( ) .IsEnabled(this, &Self::GetIsUsingGitLfsLocking) .HintText(LOCTEXT("LfsUserName_Hint", "Username to lock files on the LFS server")) ] + ] + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SHorizontalBox) + ROW_LEFT ( 10.0f ) + [ + SNew(STextBlock) + .Text(LOCTEXT("LockProvider", "Lock Provider Class")) + .ToolTipText(LOCTEXT("LockProviderTT", "Class to use for get/set of lock status")) + ] + ROW_RIGHT( 10.0f ) + [ + SNew(SClassPropertyEntryBox) + .RequiredInterface(UGitLockProvider::StaticClass()) + .AllowAbstract(false) + .AllowNone(false) + .SelectedClass(this, &Self::GetLockProviderClass) + .OnSetClass(this, &Self::SetLockProviderClass) + ] ] // [Optional] Initial Git Commit +SVerticalBox::Slot() @@ -594,7 +617,7 @@ void SGitSourceControlSettings::ConstructBasedOnEngineVersion( ) // TODO [RW] The UE5 GUI for the two optional initial git support functionalities has not been tested } #endif - +// clang-format on SGitSourceControlSettings::~SGitSourceControlSettings() { RemoveInProgressNotification(); @@ -606,16 +629,16 @@ FString SGitSourceControlSettings::GetBinaryPathString() const return GitSourceControl.AccessSettings().GetBinaryPath(); } -void SGitSourceControlSettings::OnBinaryPathPicked( const FString& PickedPath ) const +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) + if (bChanged) { // Re-Check provided git binary path for each change GitSourceControl.GetProvider().CheckGitAvailability(); - if(GitSourceControl.GetProvider().IsGitAvailable()) + if (GitSourceControl.GetProvider().IsGitAvailable()) { GitSourceControl.SaveSettings(); } @@ -643,6 +666,26 @@ FText SGitSourceControlSettings::GetUserEmail() const return FText::FromString(UserEmail); } +const UClass* SGitSourceControlSettings::GetLockProviderClass() const +{ + const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); + TSoftClassPtr LockProviderClass = GitSourceControl.AccessSettings().GetLockProviderClass(); + if (LockProviderClass.IsValid()) + { + return LockProviderClass.LoadSynchronous(); + } + else + { + return ULFSLockProvider::StaticClass(); + } +} + +void SGitSourceControlSettings::SetLockProviderClass(const UClass* Value) +{ + FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); + GitSourceControl.AccessSettings().SetLockProviderClass(Value); +} + EVisibility SGitSourceControlSettings::MustInitializeGitRepository() const { const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); @@ -688,21 +731,26 @@ FReply SGitSourceControlSettings::OnClickedInitializeGitRepository() TArray 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()) + 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 Parameters; Parameters.Add(TEXT("add origin")); Parameters.Add(RemoteUrl.ToString()); - GitSourceControlUtils::RunCommand(TEXT("remote"), PathToGitBinary, PathToProjectDir, Parameters, FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); + 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()) + if (GitSourceControl.GetProvider().IsAvailable()) { - // List of files to add to Revision Control (.uproject, Config/, Content/, Source/ files and .gitignore/.gitattributes if any) + // List of files to add to Revision Control (.uproject, Config/, Content/, Source/ files and + // .gitignore/.gitattributes if any) TArray ProjectFiles; ProjectFiles.Add(FPaths::ProjectContentDir()); ProjectFiles.Add(FPaths::ProjectConfigDir()); @@ -711,29 +759,35 @@ FReply SGitSourceControlSettings::OnClickedInitializeGitRepository() { ProjectFiles.Add(FPaths::GameSourceDir()); } - if(bAutoCreateGitIgnore) + 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)) + 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) + 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)) + if (FFileHelper::SaveStringToFile(ReadmeContent.ToString(), *ReadmeFilename, + FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) { ProjectFiles.Add(ReadmeFilename); } } - if(bAutoCreateGitAttributes) + 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); + 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")); @@ -747,7 +801,8 @@ FReply SGitSourceControlSettings::OnClickedInitializeGitRepository() { GitAttributesContent = TEXT("Content/** filter=lfs diff=lfs merge=lfs -text\n"); } - if(FFileHelper::SaveStringToFile(GitAttributesContent, *GitAttributesFilename, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) + if (FFileHelper::SaveStringToFile(GitAttributesContent, *GitAttributesFilename, + FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) { ProjectFiles.Add(GitAttributesFilename); } @@ -769,9 +824,13 @@ void SGitSourceControlSettings::LaunchMarkForAddOperation(const TArray& FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); TSharedRef MarkForAddOperation = ISourceControlOperation::Create(); #if ENGINE_MAJOR_VERSION >= 5 - ECommandResult::Type Result = GitSourceControl.GetProvider().Execute(MarkForAddOperation, FSourceControlChangelistPtr(), InFiles, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateSP(this, &SGitSourceControlSettings::OnSourceControlOperationComplete)); + 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)); + ECommandResult::Type Result = GitSourceControl.GetProvider().Execute( + MarkForAddOperation, InFiles, EConcurrency::Asynchronous, + FSourceControlOperationComplete::CreateSP(this, &SGitSourceControlSettings::OnSourceControlOperationComplete)); #endif if (Result == ECommandResult::Succeeded) { @@ -790,9 +849,14 @@ void SGitSourceControlSettings::LaunchCheckInOperation() 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)); + 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)); + ECommandResult::Type Result = GitSourceControl.GetProvider().Execute( + CheckInOperation, FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous, + FSourceControlOperationComplete::CreateSP(this, &SGitSourceControlSettings::OnSourceControlOperationComplete)); #endif if (Result == ECommandResult::Succeeded) { @@ -805,7 +869,8 @@ void SGitSourceControlSettings::LaunchCheckInOperation() } /// 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) +void SGitSourceControlSettings::OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, + ECommandResult::Type InResult) { RemoveInProgressNotification(); @@ -826,7 +891,6 @@ void SGitSourceControlSettings::OnSourceControlOperationComplete(const FSourceCo } } - // Display an ongoing notification during the whole operation void SGitSourceControlSettings::DisplayInProgressNotification(const FSourceControlOperationRef& InOperation) { @@ -854,7 +918,8 @@ void SGitSourceControlSettings::RemoveInProgressNotification() // 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())); + 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 @@ -868,7 +933,8 @@ void SGitSourceControlSettings::DisplaySuccessNotification(const FSourceControlO // 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())); + 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); diff --git a/Source/GitSourceControl/Private/SGitSourceControlSettings.h b/Source/GitSourceControl/Private/SGitSourceControlSettings.h index fa83e856..16365643 100644 --- a/Source/GitSourceControl/Private/SGitSourceControlSettings.h +++ b/Source/GitSourceControl/Private/SGitSourceControlSettings.h @@ -5,15 +5,21 @@ #pragma once -#include "Widgets/SCompoundWidget.h" #include "ISourceControlProvider.h" #include "Runtime/Launch/Resources/Version.h" +#include "Widgets/SCompoundWidget.h" class SNotificationItem; #if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 2 -namespace ETextCommit { enum Type : int; } +namespace ETextCommit +{ + enum Type : int; +} #else -namespace ETextCommit { enum Type; } +namespace ETextCommit +{ + enum Type; +} #endif enum class ECheckBoxState : uint8; @@ -21,29 +27,30 @@ 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( ); + void ConstructBasedOnEngineVersion(); /** Delegates to get Git binary path from/to settings */ FString GetBinaryPathString() const; - void OnBinaryPathPicked(const FString & PickedPath) 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; + const UClass* GetLockProviderClass() const; + void SetLockProviderClass(const UClass* Value); + EVisibility MustInitializeGitRepository() const; bool CanInitializeGitRepository() const; bool CanUseGitLfsLocking() const; @@ -92,7 +99,7 @@ class SGitSourceControlSettings : public SCompoundWidget /** Asynchronous operation progress notifications */ TWeakPtr OperationInProgressNotification; - + void DisplayInProgressNotification(const FSourceControlOperationRef& InOperation); void RemoveInProgressNotification(); void DisplaySuccessNotification(const FSourceControlOperationRef& InOperation); From 2bbe228d26f5c8c447ad78aaf331e7d60a395a25 Mon Sep 17 00:00:00 2001 From: Stephen Whittle Date: Mon, 10 Feb 2025 14:27:02 +1100 Subject: [PATCH 04/11] Everything for locks now goes through the provider --- .../GitSourceControl.Build.cs | 3 +- .../Private/GitSourceControlModule.cpp | 218 +- .../Private/GitSourceControlOperations.cpp | 349 +- .../Private/GitSourceControlUtils.cpp | 3921 +++++++++-------- .../Private/LFSLockProvider.cpp | 52 +- .../Private/LFSLockProvider.h | 9 + .../Private/ModioLockProvider.cpp | 156 + .../Private/ModioLockProvider.h | 50 + .../Private/SGitSourceControlSettings.cpp | 2 +- .../Public/GitSourceControlModule.h | 40 +- .../Public/GitSourceControlSettings.h | 7 +- .../Public/GitSourceControlUtils.h | 616 +-- .../Public/IGitLockProvider.h | 34 +- 13 files changed, 3053 insertions(+), 2404 deletions(-) create mode 100644 Source/GitSourceControl/Private/ModioLockProvider.cpp create mode 100644 Source/GitSourceControl/Private/ModioLockProvider.h diff --git a/Source/GitSourceControl/GitSourceControl.Build.cs b/Source/GitSourceControl/GitSourceControl.Build.cs index 5b58ca1c..6049ca3c 100644 --- a/Source/GitSourceControl/GitSourceControl.Build.cs +++ b/Source/GitSourceControl/GitSourceControl.Build.cs @@ -22,7 +22,8 @@ public GitSourceControl(ReadOnlyTargetRules Target) : base(Target) "SourceControl", "SourceControlWindows", "Projects", - "PropertyEditor" + "PropertyEditor", + "HTTP" } ); diff --git a/Source/GitSourceControl/Private/GitSourceControlModule.cpp b/Source/GitSourceControl/Private/GitSourceControlModule.cpp index e2171877..4f1aab3e 100644 --- a/Source/GitSourceControl/Private/GitSourceControlModule.cpp +++ b/Source/GitSourceControl/Private/GitSourceControlModule.cpp @@ -7,25 +7,26 @@ #include "AssetToolsModule.h" #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 -#include "Styling/AppStyle.h" + #include "Styling/AppStyle.h" #else -#include "EditorStyleSet.h" + #include "EditorStyleSet.h" #endif +#include "Features/IModularFeatures.h" #include "Misc/App.h" #include "Modules/ModuleManager.h" -#include "Features/IModularFeatures.h" -#include "ContentBrowserModule.h" #include "ContentBrowserDelegates.h" +#include "ContentBrowserModule.h" +#include "Framework/Commands/UIAction.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "Framework/MultiBox/MultiBoxExtender.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" +#include "LFSLockProvider.h" #include "Misc/ConfigCacheIni.h" +#include "SourceControlHelpers.h" #define LOCTEXT_NAMESPACE "GitSourceControl" @@ -33,41 +34,59 @@ TArray FGitSourceControlModule::EmptyStringArray; namespace { - static const FName NAME_SourceControl( TEXT( "SourceControl" ) ); - static const FName NAME_ContentBrowser( TEXT( "ContentBrowser" ) ); -} + static const FName NAME_SourceControl(TEXT("SourceControl")); + static const FName NAME_ContentBrowser(TEXT("ContentBrowser")); +} // namespace template static TSharedRef CreateWorker() { - return MakeShareable( new Type() ); + 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 ) ); - // 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 ) ); - GitSourceControlProvider.RegisterWorker( "UpdateStatus", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); - GitSourceControlProvider.RegisterWorker( "MarkForAdd", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); - GitSourceControlProvider.RegisterWorker( "Delete", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); - GitSourceControlProvider.RegisterWorker( "Revert", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); - GitSourceControlProvider.RegisterWorker( "Sync", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); - GitSourceControlProvider.RegisterWorker( "Fetch", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); - GitSourceControlProvider.RegisterWorker( "CheckIn", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); - GitSourceControlProvider.RegisterWorker( "Copy", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); - GitSourceControlProvider.RegisterWorker( "Resolve", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); - GitSourceControlProvider.RegisterWorker( "MoveToChangelist", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); - GitSourceControlProvider.RegisterWorker( "UpdateChangelistsStatus", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); + // Register our operations (implemented in GitSourceControlOperations.cpp by subclassing from + // Engine\Source\Developer\SourceControl\Public\SourceControlOperations.h) + GitSourceControlProvider.RegisterWorker("Connect", + FGetGitSourceControlWorker::CreateStatic(&CreateWorker)); + // 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)); + GitSourceControlProvider.RegisterWorker( + "UpdateStatus", FGetGitSourceControlWorker::CreateStatic(&CreateWorker)); + GitSourceControlProvider.RegisterWorker( + "MarkForAdd", FGetGitSourceControlWorker::CreateStatic(&CreateWorker)); + GitSourceControlProvider.RegisterWorker("Delete", + FGetGitSourceControlWorker::CreateStatic(&CreateWorker)); + GitSourceControlProvider.RegisterWorker("Revert", + FGetGitSourceControlWorker::CreateStatic(&CreateWorker)); + GitSourceControlProvider.RegisterWorker("Sync", + FGetGitSourceControlWorker::CreateStatic(&CreateWorker)); + GitSourceControlProvider.RegisterWorker("Fetch", + FGetGitSourceControlWorker::CreateStatic(&CreateWorker)); + GitSourceControlProvider.RegisterWorker("CheckIn", + FGetGitSourceControlWorker::CreateStatic(&CreateWorker)); + GitSourceControlProvider.RegisterWorker("Copy", + FGetGitSourceControlWorker::CreateStatic(&CreateWorker)); + GitSourceControlProvider.RegisterWorker("Resolve", + FGetGitSourceControlWorker::CreateStatic(&CreateWorker)); + GitSourceControlProvider.RegisterWorker( + "MoveToChangelist", FGetGitSourceControlWorker::CreateStatic(&CreateWorker)); + GitSourceControlProvider.RegisterWorker( + "UpdateChangelistsStatus", FGetGitSourceControlWorker::CreateStatic(&CreateWorker)); // load our settings GitSourceControlSettings.LoadSettings(); - // If configured, do a check if the current user has permissions to access a specified repository. Exit with a fatal error if that is the case. + // If configured, do a check if the current user has permissions to access a specified repository. Exit with a fatal + // error if that is the case. FString RequiredRepositoryAccessURL, RequiredRepositoryAccessBranchName; - GConfig->GetString(TEXT("GitSourceControl"), TEXT("RequiredAccessRepositoryURL"), RequiredRepositoryAccessURL, GEditorIni); - GConfig->GetString(TEXT("GitSourceControl"), TEXT("RequiredAccessRepositoryBranchName"), RequiredRepositoryAccessBranchName, GEditorIni); + GConfig->GetString(TEXT("GitSourceControl"), TEXT("RequiredAccessRepositoryURL"), RequiredRepositoryAccessURL, + GEditorIni); + GConfig->GetString(TEXT("GitSourceControl"), TEXT("RequiredAccessRepositoryBranchName"), + RequiredRepositoryAccessBranchName, GEditorIni); if (!RequiredRepositoryAccessURL.IsEmpty()) { if (RequiredRepositoryAccessBranchName.IsEmpty()) @@ -80,7 +99,8 @@ void FGitSourceControlModule::StartupModule() // If using SSH, will fail if user doesn't have SSH keys set up const bool bLaunchedProcess = FPlatformProcess::ExecProcess( TEXT("git"), - *FString::Format(TEXT("ls-remote --exit-code {0} {1}"), {RequiredRepositoryAccessURL, RequiredRepositoryAccessBranchName}), + *FString::Format(TEXT("ls-remote --exit-code {0} {1}"), + {RequiredRepositoryAccessURL, RequiredRepositoryAccessBranchName}), &ReturnCode, nullptr, &StdErr); if (!bLaunchedProcess) { @@ -90,34 +110,59 @@ void FGitSourceControlModule::StartupModule() { if (StdErr.IsEmpty()) { - StdErr = TEXT("Branch not found"); // if there is no output and there is a bad exit code, it's very likely the branch name was not found + StdErr = TEXT("Branch not found"); // if there is no output and there is a bad exit code, it's very + // likely the branch name was not found } UE_LOG(LogSourceControl, Fatal, TEXT("Could access branch %s on required repository %s(%d): %s"), - *RequiredRepositoryAccessBranchName, *RequiredRepositoryAccessURL, ReturnCode, *StdErr); + *RequiredRepositoryAccessBranchName, *RequiredRepositoryAccessURL, ReturnCode, *StdErr); } } // Bind our revision control provider to the editor - IModularFeatures::Get().RegisterModularFeature( NAME_SourceControl, &GitSourceControlProvider ); + IModularFeatures::Get().RegisterModularFeature(NAME_SourceControl, &GitSourceControlProvider); - FContentBrowserModule & ContentBrowserModule = FModuleManager::Get().LoadModuleChecked< FContentBrowserModule >( NAME_ContentBrowser ); + FContentBrowserModule& ContentBrowserModule = + FModuleManager::Get().LoadModuleChecked(NAME_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 + // 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&, bool ) { GitSourceControlProvider.TicksUntilNextForcedUpdate = 1; } ); - CbdHandle_OnAssetPathChanged = ContentBrowserModule.GetOnAssetPathChanged().AddLambda( [this]( const FString& ) { GitSourceControlProvider.TicksUntilNextForcedUpdate = 2; } ); + 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&, bool) { GitSourceControlProvider.TicksUntilNextForcedUpdate = 1; }); + CbdHandle_OnAssetPathChanged = ContentBrowserModule.GetOnAssetPathChanged().AddLambda( + [this](const FString&) { GitSourceControlProvider.TicksUntilNextForcedUpdate = 2; }); #endif - TArray& CBAssetMenuExtenderDelegates = ContentBrowserModule.GetAllAssetViewContextMenuExtenders(); - CBAssetMenuExtenderDelegates.Add(FContentBrowserMenuExtender_SelectedAssets::CreateRaw( this, &FGitSourceControlModule::OnExtendContentBrowserAssetSelectionMenu )); + TArray& CBAssetMenuExtenderDelegates = + ContentBrowserModule.GetAllAssetViewContextMenuExtenders(); + CBAssetMenuExtenderDelegates.Add(FContentBrowserMenuExtender_SelectedAssets::CreateRaw( + this, &FGitSourceControlModule::OnExtendContentBrowserAssetSelectionMenu)); CbdHandle_OnExtendAssetSelectionMenu = CBAssetMenuExtenderDelegates.Last().GetHandle(); + + UpdateLockProviderInstance(); +} + +void FGitSourceControlModule::UpdateLockProviderInstance() +{ + UClass* LockProviderClass = ULFSLockProvider::StaticClass(); + TSoftClassPtr LockProviderClassPtr = GitSourceControlSettings.GetLockProviderClass(); + if (LockProviderClassPtr.IsValid()) + { + LockProviderClassPtr.LoadSynchronous(); + } + + LockProvider.Reset(NewObject(GetTransientPackage(), LockProviderClass)); } void FGitSourceControlModule::ShutdownModule() @@ -126,20 +171,22 @@ void FGitSourceControlModule::ShutdownModule() GitSourceControlProvider.Close(); // unbind provider from editor - IModularFeatures::Get().UnregisterModularFeature( NAME_SourceControl, &GitSourceControlProvider ); - + IModularFeatures::Get().UnregisterModularFeature(NAME_SourceControl, &GitSourceControlProvider); // Unregister ContentBrowserDelegate Handles - FContentBrowserModule & ContentBrowserModule = FModuleManager::Get().GetModuleChecked< FContentBrowserModule >( NAME_ContentBrowser ); + FContentBrowserModule& ContentBrowserModule = + FModuleManager::Get().GetModuleChecked(NAME_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 ); + ContentBrowserModule.GetOnFilterChanged().Remove(CbdHandle_OnFilterChanged); + ContentBrowserModule.GetOnSearchBoxChanged().Remove(CbdHandle_OnSearchBoxChanged); + ContentBrowserModule.GetOnAssetSelectionChanged().Remove(CbdHandle_OnAssetSelectionChanged); + ContentBrowserModule.GetOnAssetPathChanged().Remove(CbdHandle_OnAssetPathChanged); #endif - - TArray& CBAssetMenuExtenderDelegates = ContentBrowserModule.GetAllAssetViewContextMenuExtenders(); - CBAssetMenuExtenderDelegates.RemoveAll([ &ExtenderDelegateHandle = CbdHandle_OnExtendAssetSelectionMenu ]( const FContentBrowserMenuExtender_SelectedAssets& Delegate ) { + + TArray& CBAssetMenuExtenderDelegates = + ContentBrowserModule.GetAllAssetViewContextMenuExtenders(); + CBAssetMenuExtenderDelegates.RemoveAll([&ExtenderDelegateHandle = CbdHandle_OnExtendAssetSelectionMenu]( + const FContentBrowserMenuExtender_SelectedAssets& Delegate) { return Delegate.GetHandle() == ExtenderDelegateHandle; }); } @@ -163,42 +210,58 @@ void FGitSourceControlModule::SetLastErrors(const TArray& InErrors) } } -TSharedRef FGitSourceControlModule::OnExtendContentBrowserAssetSelectionMenu(const TArray& SelectedAssets) +UGitLockProviderBase* FGitSourceControlModule::GetLockProvider() const +{ + if (!LockProvider.IsValid()) + { + return nullptr; + } + return LockProvider.Get(); +} + +void FGitSourceControlModule::SetLockProviderClass(TSoftClassPtr Provider) +{ + GitSourceControlSettings.SetLockProviderClass(Provider); + UpdateLockProviderInstance(); +} + +TSharedRef FGitSourceControlModule::OnExtendContentBrowserAssetSelectionMenu( + const TArray& SelectedAssets) { TSharedRef Extender(new FExtender()); - - Extender->AddMenuExtension( - "AssetSourceControlActions", - EExtensionHook::After, - nullptr, - FMenuExtensionDelegate::CreateRaw( this, &FGitSourceControlModule::CreateGitContentBrowserAssetMenu, SelectedAssets ) - ); + + Extender->AddMenuExtension("AssetSourceControlActions", EExtensionHook::After, nullptr, + FMenuExtensionDelegate::CreateRaw( + this, &FGitSourceControlModule::CreateGitContentBrowserAssetMenu, SelectedAssets)); return Extender; } -void FGitSourceControlModule::CreateGitContentBrowserAssetMenu(FMenuBuilder& MenuBuilder, const TArray SelectedAssets) +void FGitSourceControlModule::CreateGitContentBrowserAssetMenu(FMenuBuilder& MenuBuilder, + const TArray SelectedAssets) { if (!FGitSourceControlModule::Get().GetProvider().GetStatusBranchNames().Num()) { return; } - + const TArray& 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)), + 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 )) - ); + FUIAction(FExecuteAction::CreateRaw(this, &FGitSourceControlModule::DiffAssetAgainstGitOriginBranch, + SelectedAssets, BranchName))); } -void FGitSourceControlModule::DiffAssetAgainstGitOriginBranch(const TArray SelectedAssets, FString BranchName) const +void FGitSourceControlModule::DiffAssetAgainstGitOriginBranch(const TArray SelectedAssets, + FString BranchName) const { for (int32 AssetIdx = 0; AssetIdx < SelectedAssets.Num(); AssetIdx++) { @@ -214,11 +277,13 @@ void FGitSourceControlModule::DiffAssetAgainstGitOriginBranch(const TArray("GitSourceControl"); + const FGitSourceControlModule& GitSourceControl = + FModuleManager::GetModuleChecked("GitSourceControl"); const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath(); const FString& PathToRepositoryRoot = GitSourceControl.GetProvider().GetPathToRepositoryRoot(); @@ -227,7 +292,8 @@ void FGitSourceControlModule::DiffAgainstOriginBranch( UObject * InObject, const const FAssetToolsModule& AssetToolsModule = FModuleManager::GetModuleChecked("AssetTools"); // Get the SCC state - const FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(SourceControlHelpers::PackageFilename(InPackagePath), EStateCacheUsage::Use); + 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()) @@ -243,7 +309,8 @@ void FGitSourceControlModule::DiffAgainstOriginBranch( UObject * InObject, const // if(SourceControlState->GetHistorySize() > 0) { TArray Errors; - const auto& Revision = GitSourceControlUtils::GetOriginRevisionOnBranch(PathToGitBinary, PathToRepositoryRoot, RelativeFileName, Errors, BranchName); + const auto& Revision = GitSourceControlUtils::GetOriginRevisionOnBranch( + PathToGitBinary, PathToRepositoryRoot, RelativeFileName, Errors, BranchName); check(Revision.IsValid()); @@ -251,7 +318,8 @@ void FGitSourceControlModule::DiffAgainstOriginBranch( UObject * InObject, const if (Revision->Get(TempFileName)) { // Try and load that package - UPackage* TempPackage = LoadPackage(nullptr, *TempFileName, LOAD_ForDiff | LOAD_DisableCompileOnLoad); + UPackage* TempPackage = + LoadPackage(nullptr, *TempFileName, LOAD_ForDiff | LOAD_DisableCompileOnLoad); if (TempPackage != nullptr) { // Grab the old asset from that old package @@ -276,6 +344,6 @@ void FGitSourceControlModule::DiffAgainstOriginBranch( UObject * InObject, const } } -IMPLEMENT_MODULE( FGitSourceControlModule, GitSourceControl ); +IMPLEMENT_MODULE(FGitSourceControlModule, GitSourceControl); #undef LOCTEXT_NAMESPACE diff --git a/Source/GitSourceControl/Private/GitSourceControlOperations.cpp b/Source/GitSourceControl/Private/GitSourceControlOperations.cpp index ecf3024a..6d5dc946 100644 --- a/Source/GitSourceControl/Private/GitSourceControlOperations.cpp +++ b/Source/GitSourceControl/Private/GitSourceControlOperations.cpp @@ -5,24 +5,25 @@ #include "GitSourceControlOperations.h" -#include "Misc/Paths.h" -#include "Modules/ModuleManager.h" -#include "SourceControlOperations.h" -#include "ISourceControlModule.h" -#include "GitSourceControlModule.h" +#include "GenericPlatform/GenericPlatformFile.h" #include "GitSourceControlCommand.h" +#include "GitSourceControlModule.h" #include "GitSourceControlUtils.h" -#include "SourceControlHelpers.h" +#include "HAL/PlatformProcess.h" +#include "ISourceControlModule.h" #include "Logging/MessageLog.h" #include "Misc/MessageDialog.h" -#include "HAL/PlatformProcess.h" -#include "GenericPlatform/GenericPlatformFile.h" +#include "Misc/Paths.h" +#include "Modules/ModuleManager.h" +#include "SourceControlHelpers.h" +#include "SourceControlOperations.h" #if ENGINE_MAJOR_VERSION >= 5 -#include "HAL/PlatformFileManager.h" + #include "HAL/PlatformFileManager.h" #else -#include "HAL/PlatformFilemanager.h" + #include "HAL/PlatformFilemanager.h" #endif +#include "IGitLockProvider.h" #include #define LOCTEXT_NAMESPACE "GitSourceControl" @@ -55,7 +56,8 @@ bool FGitConnectWorker::Execute(FGitSourceControlCommand& InCommand) // 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."); + 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; @@ -63,21 +65,25 @@ bool FGitConnectWorker::Execute(FGitSourceControlCommand& InCommand) } // Get default branch: git remote show - + TArray 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 InfoMessages; TArray ErrorMessages; - InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("ls-remote"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, 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."); + 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); } @@ -113,17 +119,22 @@ bool FGitCheckOutWorker::Execute(FGitSourceControlCommand& InCommand) } // lock files: execute the LFS command on relative filenames - const TArray& RelativeFiles = GitSourceControlUtils::RelativeFilenames(InCommand.Files, InCommand.PathToGitRoot); + const TArray& RelativeFiles = + GitSourceControlUtils::RelativeFilenames(InCommand.Files, InCommand.PathToGitRoot); - const TArray& LockableRelativeFiles = RelativeFiles.FilterByPredicate(GitSourceControlUtils::IsFileLFSLockable); + const TArray& 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); + const bool bSuccess = FGitSourceControlModule::Get().GetLockProvider()->LockFiles( + InCommand.PathToGitRoot, + FGitFileLockOpParams {InCommand.PathToGitBinary, FGitSourceControlModule::GetEmptyStringArray(), false, + LockableRelativeFiles}, + InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); InCommand.bCommandSuccessful = bSuccess; const FString& LockUser = FGitSourceControlModule::Get().GetProvider().GetLockUser(); if (bSuccess) @@ -137,7 +148,8 @@ bool FGitCheckOutWorker::Execute(FGitSourceControlCommand& InCommand) AbsoluteFiles.Add(AbsoluteFile); } - GitSourceControlUtils::CollectNewStates(AbsoluteFiles, States, EFileState::Unset, ETreeState::Unset, ELockState::Locked); + GitSourceControlUtils::CollectNewStates(AbsoluteFiles, States, EFileState::Unset, ETreeState::Unset, + ELockState::Locked); for (auto& State : States) { State.Value.LockUser = LockUser; @@ -189,11 +201,14 @@ bool FGitCheckInWorker::Execute(FGitSourceControlCommand& InCommand) ParamCommitMsgFilename += FPaths::ConvertRelativePathToFull(CommitMsgFile.GetFilename()); ParamCommitMsgFilename += TEXT("\""); TArray CommitParameters {ParamCommitMsgFilename}; - const TArray& 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); + const TArray& 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 @@ -210,29 +225,39 @@ bool FGitCheckInWorker::Execute(FGitSourceControlCommand& InCommand) } } Operation->SetSuccessMessage(ParseCommitResults(InCommand.ResultInfo.InfoMessages)); - const FString& Message = (InCommand.ResultInfo.InfoMessages.Num() > 0) ? InCommand.ResultInfo.InfoMessages[0] : TEXT(""); + 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); + 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. + // 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 CommittedFiles; FString BranchName; bool bDiffSuccess; - if (GitSourceControlUtils::GetRemoteBranchName(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, BranchName)) + if (GitSourceControlUtils::GetRemoteBranchName(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, + BranchName)) { TArray 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); + 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 Parameters {"--branches", "--not" "--remotes", "--name-only", "--pretty="}; - bDiffSuccess = GitSourceControlUtils::RunCommand(TEXT("log"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters, FGitSourceControlModule::GetEmptyStringArray(), CommittedFiles, InCommand.ResultInfo.ErrorMessages); + TArray 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{CommittedFiles}.Array(); + CommittedFiles = TSet {CommittedFiles}.Array(); } bool bUnpushedFiles; @@ -257,9 +282,10 @@ bool FGitCheckInWorker::Execute(FGitSourceControlCommand& InCommand) { // TODO: configure remote TArray 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); + InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand( + TEXT("push"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, PushParameters, + FGitSourceControlModule::GetEmptyStringArray(), InCommand.ResultInfo.InfoMessages, + InCommand.ResultInfo.ErrorMessages); if (!InCommand.bCommandSuccessful) { @@ -267,10 +293,12 @@ bool FGitCheckInWorker::Execute(FGitSourceControlCommand& InCommand) 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")))) || + 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 + // Don't do it during iteration, want to append pull results to + // InCommand.ResultInfo.ErrorMessages bWasOutOfDate = true; break; } @@ -278,19 +306,22 @@ bool FGitCheckInWorker::Execute(FGitSourceControlCommand& InCommand) if (bWasOutOfDate) { // Get latest - const bool bFetched = GitSourceControlUtils::FetchRemote(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, false, - InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); + 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); + 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); + FGitSourceControlModule::GetEmptyStringArray(), InCommand.ResultInfo.InfoMessages, + InCommand.ResultInfo.ErrorMessages); } } @@ -300,17 +331,20 @@ bool FGitCheckInWorker::Execute(FGitSourceControlCommand& InCommand) 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 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")); + UE_LOG(LogSourceControl, Log, + TEXT("Push failed because we're out of date, prompting user to resolve manually")); } } } @@ -333,14 +367,17 @@ bool FGitCheckInWorker::Execute(FGitSourceControlCommand& InCommand) GitSourceControlUtils::GetLockedFiles(FilesToCheckIn.Array(), LockedFiles); if (LockedFiles.Num() > 0) { - const TArray& FilesToUnlock = GitSourceControlUtils::RelativeFilenames(LockedFiles, InCommand.PathToGitRoot); + const TArray& 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); + const bool bUnlockSuccess = FGitSourceControlModule::Get().GetLockProvider()->UnlockFiles( + InCommand.PathToGitRoot, + FGitFileLockOpParams {InCommand.PathToGitBinary, + FGitSourceControlModule::GetEmptyStringArray(), false, FilesToUnlock}, + InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); if (bUnlockSuccess) { for (const auto& File : LockedFiles) @@ -364,13 +401,15 @@ bool FGitCheckInWorker::Execute(FGitSourceControlCommand& InCommand) { FilesToCheckIn.Append(PulledFiles); } - // Before, we added only lockable files from CommittedFiles. But now, we want to update all files, not just lockables. + // 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 UpdatedStates; - bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, - FilesToCheckIn.Array(), InCommand.ResultInfo.ErrorMessages, UpdatedStates); + bool bSuccess = GitSourceControlUtils::RunUpdateStatus( + InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, + FilesToCheckIn.Array(), InCommand.ResultInfo.ErrorMessages, UpdatedStates); if (bSuccess) { GitSourceControlUtils::CollectNewStates(UpdatedStates, States); @@ -404,7 +443,10 @@ bool FGitMarkForAddWorker::Execute(FGitSourceControlCommand& InCommand) 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); + InCommand.bCommandSuccessful = + GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, + FGitSourceControlModule::GetEmptyStringArray(), InCommand.Files, + InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); if (InCommand.bCommandSuccessful) { @@ -413,7 +455,9 @@ bool FGitMarkForAddWorker::Execute(FGitSourceControlCommand& InCommand) else { TMap UpdatedStates; - bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates); + bool bSuccess = GitSourceControlUtils::RunUpdateStatus( + InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, + InCommand.ResultInfo.ErrorMessages, UpdatedStates); if (bSuccess) { GitSourceControlUtils::CollectNewStates(UpdatedStates, States); @@ -444,7 +488,10 @@ bool FGitDeleteWorker::Execute(FGitSourceControlCommand& InCommand) 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); + InCommand.bCommandSuccessful = + GitSourceControlUtils::RunCommand(TEXT("rm"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, + FGitSourceControlModule::GetEmptyStringArray(), InCommand.Files, + InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); if (InCommand.bCommandSuccessful) { @@ -453,7 +500,9 @@ bool FGitDeleteWorker::Execute(FGitSourceControlCommand& InCommand) else { TMap UpdatedStates; - bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates); + bool bSuccess = GitSourceControlUtils::RunUpdateStatus( + InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, + InCommand.ResultInfo.ErrorMessages, UpdatedStates); if (bSuccess) { GitSourceControlUtils::CollectNewStates(UpdatedStates, States); @@ -469,9 +518,9 @@ 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& InFiles, TArray& OutMissingFiles, TArray& OutAllExistingFiles, TArray& OutOtherThanAddedExistingFiles) +void GetMissingVsExistingFiles(const TArray& InFiles, TArray& OutMissingFiles, + TArray& OutAllExistingFiles, TArray& OutOtherThanAddedExistingFiles) { FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); @@ -529,35 +578,53 @@ bool FGitRevertWorker::Execute(FGitSourceControlCommand& InCommand) { TArray Parms; Parms.Add(TEXT("--hard")); - InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("reset"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parms, FGitSourceControlModule::GetEmptyStringArray(), InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); + 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); + 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); + 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); + 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 + // 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 ) + while (Attempts-- > 0) { - CheckoutSuccess = GitSourceControlUtils::RunCommand(TEXT("checkout"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), OtherThanAddedExistingFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); + CheckoutSuccess = GitSourceControlUtils::RunCommand( + TEXT("checkout"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, + FGitSourceControlModule::GetEmptyStringArray(), OtherThanAddedExistingFiles, + InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); if (CheckoutSuccess) { break; @@ -565,7 +632,7 @@ bool FGitRevertWorker::Execute(FGitSourceControlCommand& InCommand) FPlatformProcess::Sleep(0.1f); } - + InCommand.bCommandSuccessful &= CheckoutSuccess; } } @@ -578,9 +645,13 @@ bool FGitRevertWorker::Execute(FGitSourceControlCommand& InCommand) GitSourceControlUtils::GetLockedFiles(OtherThanAddedExistingFiles, LockedFiles); if (LockedFiles.Num() > 0) { - const TArray& 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); + const TArray& RelativeFiles = + GitSourceControlUtils::RelativeFilenames(LockedFiles, InCommand.PathToGitRoot); + InCommand.bCommandSuccessful &= FGitSourceControlModule::Get().GetLockProvider()->UnlockFiles( + InCommand.PathToGitRoot, + FGitFileLockOpParams {InCommand.PathToGitBinary, FGitSourceControlModule::GetEmptyStringArray(), false, + RelativeFiles}, + InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); if (InCommand.bCommandSuccessful) { for (const auto& File : LockedFiles) @@ -591,19 +662,25 @@ bool FGitRevertWorker::Execute(FGitSourceControlCommand& InCommand) } } - // 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. + // 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 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); + 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 UpdatedStates; - bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, FilesToUpdate, InCommand.ResultInfo.ErrorMessages, UpdatedStates); + bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, + InCommand.bUsingGitLfsLocking, FilesToUpdate, + InCommand.ResultInfo.ErrorMessages, UpdatedStates); if (bSuccess) { GitSourceControlUtils::CollectNewStates(UpdatedStates, States); @@ -626,24 +703,30 @@ FName FGitSyncWorker::GetName() const bool FGitSyncWorker::Execute(FGitSourceControlCommand& InCommand) { TArray Results; - const bool bFetched = GitSourceControlUtils::FetchRemote(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, false, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); + 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); + InCommand.bCommandSuccessful = + GitSourceControlUtils::PullOrigin(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.Files, + InCommand.Files, Results, InCommand.ResultInfo.ErrorMessages); // now update the status of our files TMap UpdatedStates; - const bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, - InCommand.Files, InCommand.ResultInfo.ErrorMessages, 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); + GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, + InCommand.CommitSummary); return InCommand.bCommandSuccessful; } @@ -671,8 +754,9 @@ FName FGitFetchWorker::GetName() const bool FGitFetchWorker::Execute(FGitSourceControlCommand& InCommand) { - InCommand.bCommandSuccessful = GitSourceControlUtils::FetchRemote(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, - InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); + InCommand.bCommandSuccessful = GitSourceControlUtils::FetchRemote( + InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, + InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); if (!InCommand.bCommandSuccessful) { return false; @@ -684,11 +768,13 @@ bool FGitFetchWorker::Execute(FGitSourceControlCommand& InCommand) if (Operation->bUpdateStatus) { // Now update the status of all our files - const TArray ProjectDirs {FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()),FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir()), + const TArray ProjectDirs {FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()), + FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir()), FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath())}; TMap UpdatedStates; - InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, - ProjectDirs, InCommand.ResultInfo.ErrorMessages, 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) { @@ -715,10 +801,12 @@ bool FGitUpdateStatusWorker::Execute(FGitSourceControlCommand& InCommand) TSharedRef Operation = StaticCastSharedRef(InCommand.Operation); - if(InCommand.Files.Num() > 0) + if (InCommand.Files.Num() > 0) { TMap UpdatedStates; - InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, 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) { @@ -733,12 +821,13 @@ bool FGitUpdateStatusWorker::Execute(FGitSourceControlCommand& InCommand) 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); + 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); + InCommand.bCommandSuccessful &= + GitSourceControlUtils::RunGetHistory(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, + File, false, InCommand.ResultInfo.ErrorMessages, History); Histories.Add(*File, History); } } @@ -747,10 +836,13 @@ bool FGitUpdateStatusWorker::Execute(FGitSourceControlCommand& InCommand) else { // no path provided: only update the status of assets in Content/ directory and also Config files - const TArray ProjectDirs {FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()), FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir()), + const TArray ProjectDirs {FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()), + FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir()), FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath())}; TMap UpdatedStates; - InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, ProjectDirs, InCommand.ResultInfo.ErrorMessages, 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) { @@ -758,9 +850,11 @@ bool FGitUpdateStatusWorker::Execute(FGitSourceControlCommand& InCommand) } } - GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary); + 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) + // 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; } @@ -769,15 +863,17 @@ bool FGitUpdateStatusWorker::UpdateStates() const { bool bUpdated = GitSourceControlUtils::UpdateCachedStates(States); - FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked( "GitSourceControl" ); + FGitSourceControlModule& GitSourceControl = + FModuleManager::GetModuleChecked("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) + // 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) + for (const auto& History : Histories) { TSharedRef State = Provider.GetStateInternal(History.Key); State->History = History.Value; @@ -800,8 +896,12 @@ bool FGitCopyWorker::Execute(FGitSourceControlCommand& InCommand) // 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); + // => 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) { @@ -810,7 +910,9 @@ bool FGitCopyWorker::Execute(FGitSourceControlCommand& InCommand) else { TMap UpdatedStates; - const bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, 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) { @@ -831,17 +933,21 @@ FName FGitResolveWorker::GetName() const return "Resolve"; } -bool FGitResolveWorker::Execute( class FGitSourceControlCommand& InCommand ) +bool FGitResolveWorker::Execute(class FGitSourceControlCommand& InCommand) { check(InCommand.Operation->GetName() == GetName()); // mark the conflicting files as resolved: TArray Results; - InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), InCommand.Files, Results, InCommand.ResultInfo.ErrorMessages); + 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 UpdatedStates; - const bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, 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) { @@ -872,21 +978,28 @@ bool FGitMoveToChangelistWorker::Execute(FGitSourceControlCommand& InCommand) FGitSourceControlChangelist DestChangelist = InCommand.Changelist; bool bResult = false; - if(DestChangelist.GetName().Equals(TEXT("Staged"))) + 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); + 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"))) + else if (DestChangelist.GetName().Equals(TEXT("Working"))) { TArray Parameter; Parameter.Add(TEXT("--staged")); - bResult = GitSourceControlUtils::RunCommand(TEXT("restore"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameter, InCommand.Files, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); + bResult = GitSourceControlUtils::RunCommand( + TEXT("restore"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameter, InCommand.Files, + InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); } - + if (bResult) { TMap DummyStates; - GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.InfoMessages, DummyStates); + GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, + InCommand.bUsingGitLfsLocking, InCommand.Files, + InCommand.ResultInfo.InfoMessages, DummyStates); } return bResult; } diff --git a/Source/GitSourceControl/Private/GitSourceControlUtils.cpp b/Source/GitSourceControl/Private/GitSourceControlUtils.cpp index 80ade6c6..efdd81c3 100644 --- a/Source/GitSourceControl/Private/GitSourceControlUtils.cpp +++ b/Source/GitSourceControl/Private/GitSourceControlUtils.cpp @@ -13,48 +13,49 @@ #include "HAL/PlatformFile.h" #if ENGINE_MAJOR_VERSION >= 5 -#include "HAL/PlatformFileManager.h" + #include "HAL/PlatformFileManager.h" #else -#include "HAL/PlatformFilemanager.h" + #include "HAL/PlatformFilemanager.h" #endif +#include "GitSourceControlChangelistState.h" +#include "GitSourceControlModule.h" #include "HAL/PlatformProcess.h" -#include "Interfaces/IPluginManager.h" -#include "ISourceControlModule.h" -#include "Misc/FileHelper.h" -#include "Misc/Paths.h" #include "ISourceControlModule.h" -#include "GitSourceControlModule.h" -#include "GitSourceControlChangelistState.h" +#include "Interfaces/IPluginManager.h" #include "Logging/MessageLog.h" #include "Misc/DateTime.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" #include "Misc/ScopeLock.h" #include "Misc/Timespan.h" -#include "PackageTools.h" #include "FileHelpers.h" #include "Misc/MessageDialog.h" +#include "PackageTools.h" #include "UObject/ObjectSaveContext.h" #include "Async/Async.h" +#include "IGitLockProvider.h" #include "UObject/Linker.h" #ifndef GIT_DEBUG_STATUS -#define GIT_DEBUG_STATUS 0 + #define GIT_DEBUG_STATUS 0 #endif #define LOCTEXT_NAMESPACE "GitSourceControl" namespace GitSourceControlConstants { -/** The maximum number of files we submit in a single Git command */ -const int32 MaxFilesPerBatch = 50; + /** The maximum number of files we submit in a single Git command */ + const int32 MaxFilesPerBatch = 50; } // namespace GitSourceControlConstants FGitScopedTempFile::FGitScopedTempFile(const FText& InText) { Filename = FPaths::CreateTempFilename(*FPaths::ProjectLogDir(), TEXT("Git-Temp"), TEXT(".txt")); - if (!FFileHelper::SaveStringToFile(InText.ToString(), *Filename, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) + if (!FFileHelper::SaveStringToFile(InText.ToString(), *Filename, + FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) { UE_LOG(LogSourceControl, Error, TEXT("Failed to write to temp file: %s"), *Filename); } @@ -80,7 +81,7 @@ FDateTime FGitLockedFilesCache::LastUpdated = FDateTime::MinValue(); TMap FGitLockedFilesCache::LockedFiles = TMap(); void FGitLockedFilesCache::SetLockedFiles(const TMap& newLocks) -{ +{ for (auto lock : LockedFiles) { if (!newLocks.Contains(lock.Key)) @@ -88,13 +89,13 @@ void FGitLockedFilesCache::SetLockedFiles(const TMap& newLocks OnFileLockChanged(lock.Key, lock.Value, false); } } - + for (auto lock : newLocks) - { + { if (!LockedFiles.Contains(lock.Key)) { OnFileLockChanged(lock.Key, lock.Value, true); - } + } } LockedFiles = newLocks; @@ -118,7 +119,7 @@ void FGitLockedFilesCache::OnFileLockChanged(const FString& filePath, const FStr const FString& LfsUserName = FGitSourceControlModule::Get().GetProvider().GetLockUser(); if (LfsUserName == lockUser) { - FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*filePath, !locked); + FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*filePath, !locked); } } @@ -142,13 +143,17 @@ namespace GitSourceControlUtils if (TestPath.IsEmpty()) { - // TestPath.IsEmpty() meaning is that FilePath is not git file. So it need to removed to git command file list. + // TestPath.IsEmpty() meaning is that FilePath is not git file. So it need to removed to git command + // file list. PackageNotIncludedInGit.Add(FilePath); - UE_LOG(LogSourceControl, Warning, TEXT("Package file to update has included dependent file is not git or Can't find directory path for file : %s"), *FilePath); + UE_LOG(LogSourceControl, Warning, + TEXT("Package file to update has included dependent file is not git or Can't find directory " + "path for file : %s"), + *FilePath); break; } - + FString GitTestPath = TestPath + "/.git"; if (FPaths::FileExists(GitTestPath) || FPaths::DirectoryExists(GitTestPath)) { @@ -156,7 +161,8 @@ namespace GitSourceControlUtils FPaths::NormalizeDirectoryName(RetNormalized); FString PathToRepositoryRootNormalized = PathToRepositoryRoot; FPaths::NormalizeDirectoryName(PathToRepositoryRootNormalized); - if (!FPaths::IsSamePath(RetNormalized, PathToRepositoryRootNormalized) && Ret != FPaths::GetPath(GitTestPath)) + if (!FPaths::IsSamePath(RetNormalized, PathToRepositoryRootNormalized) && + Ret != FPaths::GetPath(GitTestPath)) { UE_LOG(LogSourceControl, Error, TEXT("Selected files belong to different submodules")); return PathToRepositoryRoot; @@ -177,1663 +183,1769 @@ namespace GitSourceControlUtils return Ret; } - FString ChangeRepositoryRootIfSubmodule(FString & AbsoluteFilePath, const FString& PathToRepositoryRoot) + FString ChangeRepositoryRootIfSubmodule(FString& AbsoluteFilePath, const FString& PathToRepositoryRoot) { - TArray AbsoluteFilePaths = { AbsoluteFilePath }; + TArray AbsoluteFilePaths = {AbsoluteFilePath}; return ChangeRepositoryRootIfSubmodule(AbsoluteFilePaths, PathToRepositoryRoot); } -// Launch the Git command line process and extract its results & errors -bool RunCommandInternalRaw(const FString& InCommand, const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InParameters, const TArray& InFiles, FString& OutResults, FString& OutErrors, const int32 ExpectedReturnCode /* = 0 */) -{ - int32 ReturnCode = 0; - FString FullCommand; - FString LogableCommand; // short version of the command for logging purpose - - if (!InRepositoryRoot.IsEmpty()) + // Launch the Git command line process and extract its results & errors + bool RunCommandInternalRaw(const FString& InCommand, const FString& InPathToGitBinary, + const FString& InRepositoryRoot, const TArray& InParameters, + const TArray& InFiles, FString& OutResults, FString& OutErrors, + const int32 ExpectedReturnCode /* = 0 */) { - FString RepositoryRoot = InRepositoryRoot; + int32 ReturnCode = 0; + FString FullCommand; + FString LogableCommand; // short version of the command for logging purpose - // Detect a "migrate asset" scenario (a "git add" command is applied to files outside the current project) - if ((InFiles.Num() > 0) && !FPaths::IsRelative(InFiles[0]) && !InFiles[0].StartsWith(InRepositoryRoot)) + if (!InRepositoryRoot.IsEmpty()) { - // in this case, find the git repository (if any) of the destination Project - FString DestinationRepositoryRoot; - if (FindRootDirectory(FPaths::GetPath(InFiles[0]), DestinationRepositoryRoot)) + FString RepositoryRoot = InRepositoryRoot; + + // Detect a "migrate asset" scenario (a "git add" command is applied to files outside the current project) + if ((InFiles.Num() > 0) && !FPaths::IsRelative(InFiles[0]) && !InFiles[0].StartsWith(InRepositoryRoot)) { - RepositoryRoot = DestinationRepositoryRoot; // if found use it for the "add" command (else not, to avoid producing one more error in logs) + // in this case, find the git repository (if any) of the destination Project + FString DestinationRepositoryRoot; + if (FindRootDirectory(FPaths::GetPath(InFiles[0]), DestinationRepositoryRoot)) + { + RepositoryRoot = DestinationRepositoryRoot; // if found use it for the "add" command (else not, to + // avoid producing one more error in logs) + } } - } - // Specify the working copy (the root) of the git repository (before the command itself) - FullCommand = TEXT("-C \""); - FullCommand += RepositoryRoot; - FullCommand += TEXT("\" "); - } - // then the git command itself ("status", "log", "commit"...) - LogableCommand += InCommand; + // Specify the working copy (the root) of the git repository (before the command itself) + FullCommand = TEXT("-C \""); + FullCommand += RepositoryRoot; + FullCommand += TEXT("\" "); + } + // then the git command itself ("status", "log", "commit"...) + LogableCommand += InCommand; - // Append to the command all parameters, and then finally the files - for (const auto& Parameter : InParameters) - { - LogableCommand += TEXT(" "); - LogableCommand += Parameter; - } - for (const auto& File : InFiles) - { - LogableCommand += TEXT(" \""); - LogableCommand += File; - LogableCommand += TEXT("\""); - } - // Also, Git does not have a "--non-interactive" option, as it auto-detects when there are no connected standard input/output streams + // Append to the command all parameters, and then finally the files + for (const auto& Parameter : InParameters) + { + LogableCommand += TEXT(" "); + LogableCommand += Parameter; + } + for (const auto& File : InFiles) + { + LogableCommand += TEXT(" \""); + LogableCommand += File; + LogableCommand += TEXT("\""); + } + // Also, Git does not have a "--non-interactive" option, as it auto-detects when there are no connected standard + // input/output streams - FullCommand += LogableCommand; + FullCommand += LogableCommand; #if UE_BUILD_DEBUG - UE_LOG(LogSourceControl, Log, TEXT("RunCommand: 'git %s'"), *LogableCommand); + UE_LOG(LogSourceControl, Log, TEXT("RunCommand: 'git %s'"), *LogableCommand); #endif - FString PathToGitOrEnvBinary = InPathToGitBinary; + FString PathToGitOrEnvBinary = InPathToGitBinary; #if PLATFORM_MAC - // The Cocoa application does not inherit shell environment variables, so add the path expected to have git-lfs to PATH - FString PathEnv = FPlatformMisc::GetEnvironmentVariable(TEXT("PATH")); - FString GitInstallPath = FPaths::GetPath(InPathToGitBinary); - - TArray PathArray; - PathEnv.ParseIntoArray(PathArray, FPlatformMisc::GetPathVarDelimiter()); - bool bHasGitInstallPath = false; - for (auto Path : PathArray) - { - if (GitInstallPath.Equals(Path, ESearchCase::CaseSensitive)) + // The Cocoa application does not inherit shell environment variables, so add the path expected to have git-lfs + // to PATH + FString PathEnv = FPlatformMisc::GetEnvironmentVariable(TEXT("PATH")); + FString GitInstallPath = FPaths::GetPath(InPathToGitBinary); + + TArray PathArray; + PathEnv.ParseIntoArray(PathArray, FPlatformMisc::GetPathVarDelimiter()); + bool bHasGitInstallPath = false; + for (auto Path : PathArray) { - bHasGitInstallPath = true; - break; + if (GitInstallPath.Equals(Path, ESearchCase::CaseSensitive)) + { + bHasGitInstallPath = true; + break; + } } - } - if (!bHasGitInstallPath) - { - PathToGitOrEnvBinary = FString("/usr/bin/env"); - FullCommand = FString::Printf(TEXT("PATH=\"%s%s%s\" \"%s\" %s"), *GitInstallPath, FPlatformMisc::GetPathVarDelimiter(), *PathEnv, *InPathToGitBinary, *FullCommand); - } + if (!bHasGitInstallPath) + { + PathToGitOrEnvBinary = FString("/usr/bin/env"); + FullCommand = + FString::Printf(TEXT("PATH=\"%s%s%s\" \"%s\" %s"), *GitInstallPath, + FPlatformMisc::GetPathVarDelimiter(), *PathEnv, *InPathToGitBinary, *FullCommand); + } #endif - FPlatformProcess::ExecProcess(*PathToGitOrEnvBinary, *FullCommand, &ReturnCode, &OutResults, &OutErrors); + FPlatformProcess::ExecProcess(*PathToGitOrEnvBinary, *FullCommand, &ReturnCode, &OutResults, &OutErrors); #if UE_BUILD_DEBUG - // TODO: add a setting to easily enable Verbose logging - UE_LOG(LogSourceControl, Verbose, TEXT("RunCommand(%s):\n%s"), *InCommand, *OutResults); - if (ReturnCode != ExpectedReturnCode) - { - UE_LOG(LogSourceControl, Warning, TEXT("RunCommand(%s) ReturnCode=%d:\n%s"), *InCommand, ReturnCode, *OutErrors); - } + // TODO: add a setting to easily enable Verbose logging + UE_LOG(LogSourceControl, Verbose, TEXT("RunCommand(%s):\n%s"), *InCommand, *OutResults); + if (ReturnCode != ExpectedReturnCode) + { + UE_LOG(LogSourceControl, Warning, TEXT("RunCommand(%s) ReturnCode=%d:\n%s"), *InCommand, ReturnCode, + *OutErrors); + } #endif - // Move push/pull progress information from the error stream to the info stream - if(ReturnCode == ExpectedReturnCode && OutErrors.Len() > 0) - { - OutResults.Append(OutErrors); - OutErrors.Empty(); - } - - return ReturnCode == ExpectedReturnCode; -} + // Move push/pull progress information from the error stream to the info stream + if (ReturnCode == ExpectedReturnCode && OutErrors.Len() > 0) + { + OutResults.Append(OutErrors); + OutErrors.Empty(); + } -// Basic parsing or results & errors from the Git command line process -static bool RunCommandInternal(const FString& InCommand, const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InParameters, - const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages) -{ - bool bResult; - FString Results; - FString Errors; + return ReturnCode == ExpectedReturnCode; + } - bResult = RunCommandInternalRaw(InCommand, InPathToGitBinary, InRepositoryRoot, InParameters, InFiles, Results, Errors); - Results.ParseIntoArray(OutResults, TEXT("\n"), true); - Errors.ParseIntoArray(OutErrorMessages, TEXT("\n"), true); + // Basic parsing or results & errors from the Git command line process + static bool RunCommandInternal(const FString& InCommand, const FString& InPathToGitBinary, + const FString& InRepositoryRoot, const TArray& InParameters, + const TArray& InFiles, TArray& OutResults, + TArray& OutErrorMessages) + { + bool bResult; + FString Results; + FString Errors; - return bResult; -} + bResult = RunCommandInternalRaw(InCommand, InPathToGitBinary, InRepositoryRoot, InParameters, InFiles, Results, + Errors); + Results.ParseIntoArray(OutResults, TEXT("\n"), true); + Errors.ParseIntoArray(OutErrorMessages, TEXT("\n"), true); -FString FindGitBinaryPath() -{ -#if PLATFORM_WINDOWS - // 1) First of all, look into standard install directories - // NOTE using only "git" (or "git.exe") relying on the "PATH" envvar does not always work as expected, depending on the installation: - // If the PATH is set with "git/cmd" instead of "git/bin", - // "git.exe" launch "git/cmd/git.exe" that redirect to "git/bin/git.exe" and ExecProcess() is unable to catch its outputs streams. - // First check the 64-bit program files directory: - FString GitBinaryPath(TEXT("C:/Program Files/Git/bin/git.exe")); - bool bFound = CheckGitAvailability(GitBinaryPath); - if (!bFound) - { - // otherwise check the 32-bit program files directory. - GitBinaryPath = TEXT("C:/Program Files (x86)/Git/bin/git.exe"); - bFound = CheckGitAvailability(GitBinaryPath); - } - if (!bFound) - { - // else the install dir for the current user: C:\Users\UserName\AppData\Local\Programs\Git\cmd - const FString AppDataLocalPath = FPlatformMisc::GetEnvironmentVariable(TEXT("LOCALAPPDATA")); - GitBinaryPath = FString::Printf(TEXT("%s/Programs/Git/cmd/git.exe"), *AppDataLocalPath); - bFound = CheckGitAvailability(GitBinaryPath); + return bResult; } - // 2) Else, look for the version of Git bundled with SmartGit "Installer with JRE" - if (!bFound) + FString FindGitBinaryPath() { - GitBinaryPath = TEXT("C:/Program Files (x86)/SmartGit/git/bin/git.exe"); - bFound = CheckGitAvailability(GitBinaryPath); +#if PLATFORM_WINDOWS + // 1) First of all, look into standard install directories + // NOTE using only "git" (or "git.exe") relying on the "PATH" envvar does not always work as expected, depending + // on the installation: If the PATH is set with "git/cmd" instead of "git/bin", "git.exe" launch + // "git/cmd/git.exe" that redirect to "git/bin/git.exe" and ExecProcess() is unable to catch its outputs + // streams. First check the 64-bit program files directory: + FString GitBinaryPath(TEXT("C:/Program Files/Git/bin/git.exe")); + bool bFound = CheckGitAvailability(GitBinaryPath); if (!bFound) { - // If git is not found in "git/bin/" subdirectory, try the "bin/" path that was in use before - GitBinaryPath = TEXT("C:/Program Files (x86)/SmartGit/bin/git.exe"); + // otherwise check the 32-bit program files directory. + GitBinaryPath = TEXT("C:/Program Files (x86)/Git/bin/git.exe"); bFound = CheckGitAvailability(GitBinaryPath); } - } - - // 3) Else, look for the local_git provided by SourceTree - if (!bFound) - { - // C:\Users\UserName\AppData\Local\Atlassian\SourceTree\git_local\bin - const FString AppDataLocalPath = FPlatformMisc::GetEnvironmentVariable(TEXT("LOCALAPPDATA")); - GitBinaryPath = FString::Printf(TEXT("%s/Atlassian/SourceTree/git_local/bin/git.exe"), *AppDataLocalPath); - bFound = CheckGitAvailability(GitBinaryPath); - } - - // 4) Else, look for the PortableGit provided by GitHub Desktop - if (!bFound) - { - // The latest GitHub Desktop adds its binaries into the local appdata directory: - // C:\Users\UserName\AppData\Local\GitHub\PortableGit_c2ba306e536fdf878271f7fe636a147ff37326ad\cmd - const FString AppDataLocalPath = FPlatformMisc::GetEnvironmentVariable(TEXT("LOCALAPPDATA")); - const FString SearchPath = FString::Printf(TEXT("%s/GitHub/PortableGit_*"), *AppDataLocalPath); - TArray PortableGitFolders; - IFileManager::Get().FindFiles(PortableGitFolders, *SearchPath, false, true); - if (PortableGitFolders.Num() > 0) - { - // FindFiles just returns directory names, so we need to prepend the root path to get the full path. - GitBinaryPath = FString::Printf(TEXT("%s/GitHub/%s/cmd/git.exe"), *AppDataLocalPath, *(PortableGitFolders.Last())); // keep only the last PortableGit found + if (!bFound) + { + // else the install dir for the current user: C:\Users\UserName\AppData\Local\Programs\Git\cmd + const FString AppDataLocalPath = FPlatformMisc::GetEnvironmentVariable(TEXT("LOCALAPPDATA")); + GitBinaryPath = FString::Printf(TEXT("%s/Programs/Git/cmd/git.exe"), *AppDataLocalPath); bFound = CheckGitAvailability(GitBinaryPath); - if (!bFound) - { - // If Portable git is not found in "cmd/" subdirectory, try the "bin/" path that was in use before - GitBinaryPath = FString::Printf(TEXT("%s/GitHub/%s/bin/git.exe"), *AppDataLocalPath, *(PortableGitFolders.Last())); // keep only the last - // PortableGit found - bFound = CheckGitAvailability(GitBinaryPath); - } } - } - - // 5) Else, look for the version of Git bundled with Tower - if (!bFound) - { - GitBinaryPath = TEXT("C:/Program Files (x86)/fournova/Tower/vendor/Git/bin/git.exe"); - bFound = CheckGitAvailability(GitBinaryPath); - } - // 6) Else, look for the PortableGit provided by Fork - if (!bFound) - { - // The latest Fork adds its binaries into the local appdata directory: - // C:\Users\UserName\AppData\Local\Fork\gitInstance\2.39.1\cmd - const FString AppDataLocalPath = FPlatformMisc::GetEnvironmentVariable(TEXT("LOCALAPPDATA")); - const FString SearchPath = FString::Printf(TEXT("%s/Fork/gitInstance/*"), *AppDataLocalPath); - TArray PortableGitFolders; - IFileManager::Get().FindFiles(PortableGitFolders, *SearchPath, false, true); - if (PortableGitFolders.Num() > 0) - { - // FindFiles just returns directory names, so we need to prepend the root path to get the full path. - GitBinaryPath = FString::Printf(TEXT("%s/Fork/gitInstance/%s/cmd/git.exe"), *AppDataLocalPath, *(PortableGitFolders.Last())); // keep only the last PortableGit found + // 2) Else, look for the version of Git bundled with SmartGit "Installer with JRE" + if (!bFound) + { + GitBinaryPath = TEXT("C:/Program Files (x86)/SmartGit/git/bin/git.exe"); bFound = CheckGitAvailability(GitBinaryPath); if (!bFound) { - // If Portable git is not found in "cmd/" subdirectory, try the "bin/" path that was in use before - GitBinaryPath = FString::Printf(TEXT("%s/Fork/gitInstance/%s/bin/git.exe"), *AppDataLocalPath, *(PortableGitFolders.Last())); // keep only the last - // PortableGit found + // If git is not found in "git/bin/" subdirectory, try the "bin/" path that was in use before + GitBinaryPath = TEXT("C:/Program Files (x86)/SmartGit/bin/git.exe"); bFound = CheckGitAvailability(GitBinaryPath); } } - } - -#elif PLATFORM_MAC - // 1) First of all, look for the version of git provided by official git - FString GitBinaryPath = TEXT("/usr/local/git/bin/git"); - bool bFound = CheckGitAvailability(GitBinaryPath); - - // 2) Else, look for the version of git provided by Homebrew - if (!bFound) - { - GitBinaryPath = TEXT("/usr/local/bin/git"); - bFound = CheckGitAvailability(GitBinaryPath); - } - - // 3) Else, look for the version of git provided by MacPorts - if (!bFound) - { - GitBinaryPath = TEXT("/opt/local/bin/git"); - bFound = CheckGitAvailability(GitBinaryPath); - } - - // 4) Else, look for the version of git provided by Command Line Tools - if (!bFound) - { - GitBinaryPath = TEXT("/usr/bin/git"); - bFound = CheckGitAvailability(GitBinaryPath); - } - { - SCOPED_AUTORELEASE_POOL; - NSWorkspace* SharedWorkspace = [NSWorkspace sharedWorkspace]; - - // 5) Else, look for the version of local_git provided by SmartGit + // 3) Else, look for the local_git provided by SourceTree if (!bFound) { - NSURL* AppURL = [SharedWorkspace URLForApplicationWithBundleIdentifier:@"com.syntevo.smartgit"]; - if (AppURL != nullptr) - { - NSBundle* Bundle = [NSBundle bundleWithURL:AppURL]; - GitBinaryPath = FString::Printf(TEXT("%s/git/bin/git"), *FString([Bundle resourcePath])); - bFound = CheckGitAvailability(GitBinaryPath); - } + // C:\Users\UserName\AppData\Local\Atlassian\SourceTree\git_local\bin + const FString AppDataLocalPath = FPlatformMisc::GetEnvironmentVariable(TEXT("LOCALAPPDATA")); + GitBinaryPath = FString::Printf(TEXT("%s/Atlassian/SourceTree/git_local/bin/git.exe"), *AppDataLocalPath); + bFound = CheckGitAvailability(GitBinaryPath); } - // 6) Else, look for the version of local_git provided by SourceTree + // 4) Else, look for the PortableGit provided by GitHub Desktop if (!bFound) { - NSURL* AppURL = [SharedWorkspace URLForApplicationWithBundleIdentifier:@"com.torusknot.SourceTreeNotMAS"]; - if (AppURL != nullptr) + // The latest GitHub Desktop adds its binaries into the local appdata directory: + // C:\Users\UserName\AppData\Local\GitHub\PortableGit_c2ba306e536fdf878271f7fe636a147ff37326ad\cmd + const FString AppDataLocalPath = FPlatformMisc::GetEnvironmentVariable(TEXT("LOCALAPPDATA")); + const FString SearchPath = FString::Printf(TEXT("%s/GitHub/PortableGit_*"), *AppDataLocalPath); + TArray PortableGitFolders; + IFileManager::Get().FindFiles(PortableGitFolders, *SearchPath, false, true); + if (PortableGitFolders.Num() > 0) { - NSBundle* Bundle = [NSBundle bundleWithURL:AppURL]; - GitBinaryPath = FString::Printf(TEXT("%s/git_local/bin/git"), *FString([Bundle resourcePath])); + // FindFiles just returns directory names, so we need to prepend the root path to get the full path. + GitBinaryPath = FString::Printf(TEXT("%s/GitHub/%s/cmd/git.exe"), *AppDataLocalPath, + *(PortableGitFolders.Last())); // keep only the last PortableGit found bFound = CheckGitAvailability(GitBinaryPath); + if (!bFound) + { + // If Portable git is not found in "cmd/" subdirectory, try the "bin/" path that was in use before + GitBinaryPath = FString::Printf(TEXT("%s/GitHub/%s/bin/git.exe"), *AppDataLocalPath, + *(PortableGitFolders.Last())); // keep only the last + // PortableGit found + bFound = CheckGitAvailability(GitBinaryPath); + } } } - // 7) Else, look for the version of local_git provided by GitHub Desktop + // 5) Else, look for the version of Git bundled with Tower if (!bFound) { - NSURL* AppURL = [SharedWorkspace URLForApplicationWithBundleIdentifier:@"com.github.GitHubClient"]; - if (AppURL != nullptr) - { - NSBundle* Bundle = [NSBundle bundleWithURL:AppURL]; - GitBinaryPath = FString::Printf(TEXT("%s/app/git/bin/git"), *FString([Bundle resourcePath])); - bFound = CheckGitAvailability(GitBinaryPath); - } + GitBinaryPath = TEXT("C:/Program Files (x86)/fournova/Tower/vendor/Git/bin/git.exe"); + bFound = CheckGitAvailability(GitBinaryPath); } - // 8) Else, look for the version of local_git provided by Tower2 + // 6) Else, look for the PortableGit provided by Fork if (!bFound) { - NSURL* AppURL = [SharedWorkspace URLForApplicationWithBundleIdentifier:@"com.fournova.Tower2"]; - if (AppURL != nullptr) + // The latest Fork adds its binaries into the local appdata directory: + // C:\Users\UserName\AppData\Local\Fork\gitInstance\2.39.1\cmd + const FString AppDataLocalPath = FPlatformMisc::GetEnvironmentVariable(TEXT("LOCALAPPDATA")); + const FString SearchPath = FString::Printf(TEXT("%s/Fork/gitInstance/*"), *AppDataLocalPath); + TArray PortableGitFolders; + IFileManager::Get().FindFiles(PortableGitFolders, *SearchPath, false, true); + if (PortableGitFolders.Num() > 0) { - NSBundle* Bundle = [NSBundle bundleWithURL:AppURL]; - GitBinaryPath = FString::Printf(TEXT("%s/git/bin/git"), *FString([Bundle resourcePath])); + // FindFiles just returns directory names, so we need to prepend the root path to get the full path. + GitBinaryPath = FString::Printf(TEXT("%s/Fork/gitInstance/%s/cmd/git.exe"), *AppDataLocalPath, + *(PortableGitFolders.Last())); // keep only the last PortableGit found bFound = CheckGitAvailability(GitBinaryPath); + if (!bFound) + { + // If Portable git is not found in "cmd/" subdirectory, try the "bin/" path that was in use before + GitBinaryPath = FString::Printf(TEXT("%s/Fork/gitInstance/%s/bin/git.exe"), *AppDataLocalPath, + *(PortableGitFolders.Last())); // keep only the last + // PortableGit found + bFound = CheckGitAvailability(GitBinaryPath); + } } } - } - -#else - FString GitBinaryPath = TEXT("/usr/bin/git"); - bool bFound = CheckGitAvailability(GitBinaryPath); -#endif - if (bFound) - { - FPaths::MakePlatformFilename(GitBinaryPath); - } - else - { - // If we did not find a path to Git, set it empty - GitBinaryPath.Empty(); - } +#elif PLATFORM_MAC + // 1) First of all, look for the version of git provided by official git + FString GitBinaryPath = TEXT("/usr/local/git/bin/git"); + bool bFound = CheckGitAvailability(GitBinaryPath); - return GitBinaryPath; -} + // 2) Else, look for the version of git provided by Homebrew + if (!bFound) + { + GitBinaryPath = TEXT("/usr/local/bin/git"); + bFound = CheckGitAvailability(GitBinaryPath); + } -bool CheckGitAvailability(const FString& InPathToGitBinary, FGitVersion* OutVersion) -{ - FString InfoMessages; - FString ErrorMessages; - bool bGitAvailable = RunCommandInternalRaw(TEXT("version"), InPathToGitBinary, FString(), FGitSourceControlModule::GetEmptyStringArray(), FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); - if (bGitAvailable) - { - if (!InfoMessages.StartsWith("git version")) + // 3) Else, look for the version of git provided by MacPorts + if (!bFound) { - bGitAvailable = false; + GitBinaryPath = TEXT("/opt/local/bin/git"); + bFound = CheckGitAvailability(GitBinaryPath); } - else if (OutVersion) + + // 4) Else, look for the version of git provided by Command Line Tools + if (!bFound) { - ParseGitVersion(InfoMessages, OutVersion); + GitBinaryPath = TEXT("/usr/bin/git"); + bFound = CheckGitAvailability(GitBinaryPath); } - } - return bGitAvailable; -} + { + SCOPED_AUTORELEASE_POOL; + NSWorkspace* SharedWorkspace = [NSWorkspace sharedWorkspace]; -void ParseGitVersion(const FString& InVersionString, FGitVersion* OutVersion) -{ -#if UE_BUILD_DEBUG - // Parse "git version 2.31.1.vfs.0.3" into the string "2.31.1.vfs.0.3" - const FString& TokenVersionStringPtr = InVersionString.RightChop(12); - if (!TokenVersionStringPtr.IsEmpty()) - { - // Parse the version into its numerical components - TArray ParsedVersionString; - TokenVersionStringPtr.ParseIntoArray(ParsedVersionString, TEXT(".")); - const int Num = ParsedVersionString.Num(); - if (Num >= 3) - { - if (ParsedVersionString[0].IsNumeric() && ParsedVersionString[1].IsNumeric() && ParsedVersionString[2].IsNumeric()) - { - OutVersion->Major = FCString::Atoi(*ParsedVersionString[0]); - OutVersion->Minor = FCString::Atoi(*ParsedVersionString[1]); - OutVersion->Patch = FCString::Atoi(*ParsedVersionString[2]); - if (Num >= 5) + // 5) Else, look for the version of local_git provided by SmartGit + if (!bFound) + { + NSURL* AppURL = [SharedWorkspace URLForApplicationWithBundleIdentifier:@"com.syntevo.smartgit"]; + if (AppURL != nullptr) { - // If labeled with fork - if (!ParsedVersionString[3].IsNumeric()) - { - OutVersion->Fork = ParsedVersionString[3]; - OutVersion->bIsFork = true; - OutVersion->ForkMajor = FCString::Atoi(*ParsedVersionString[4]); - if (Num >= 6) - { - OutVersion->ForkMinor = FCString::Atoi(*ParsedVersionString[5]); - if (Num >= 7) - { - OutVersion->ForkPatch = FCString::Atoi(*ParsedVersionString[6]); - } - } - } + NSBundle* Bundle = [NSBundle bundleWithURL:AppURL]; + GitBinaryPath = FString::Printf(TEXT("%s/git/bin/git"), *FString([Bundle resourcePath])); + bFound = CheckGitAvailability(GitBinaryPath); } - if (OutVersion->bIsFork) + } + + // 6) Else, look for the version of local_git provided by SourceTree + if (!bFound) + { + NSURL* AppURL = + [SharedWorkspace URLForApplicationWithBundleIdentifier:@"com.torusknot.SourceTreeNotMAS"]; + if (AppURL != nullptr) { - UE_LOG(LogSourceControl, Log, TEXT("Git version %d.%d.%d.%s.%d.%d.%d"), OutVersion->Major, OutVersion->Minor, OutVersion->Patch, *OutVersion->Fork, OutVersion->ForkMajor, OutVersion->ForkMinor, OutVersion->ForkPatch); + NSBundle* Bundle = [NSBundle bundleWithURL:AppURL]; + GitBinaryPath = FString::Printf(TEXT("%s/git_local/bin/git"), *FString([Bundle resourcePath])); + bFound = CheckGitAvailability(GitBinaryPath); } - else + } + + // 7) Else, look for the version of local_git provided by GitHub Desktop + if (!bFound) + { + NSURL* AppURL = [SharedWorkspace URLForApplicationWithBundleIdentifier:@"com.github.GitHubClient"]; + if (AppURL != nullptr) + { + NSBundle* Bundle = [NSBundle bundleWithURL:AppURL]; + GitBinaryPath = FString::Printf(TEXT("%s/app/git/bin/git"), *FString([Bundle resourcePath])); + bFound = CheckGitAvailability(GitBinaryPath); + } + } + + // 8) Else, look for the version of local_git provided by Tower2 + if (!bFound) + { + NSURL* AppURL = [SharedWorkspace URLForApplicationWithBundleIdentifier:@"com.fournova.Tower2"]; + if (AppURL != nullptr) { - UE_LOG(LogSourceControl, Log, TEXT("Git version %d.%d.%d"), OutVersion->Major, OutVersion->Minor, OutVersion->Patch); + NSBundle* Bundle = [NSBundle bundleWithURL:AppURL]; + GitBinaryPath = FString::Printf(TEXT("%s/git/bin/git"), *FString([Bundle resourcePath])); + bFound = CheckGitAvailability(GitBinaryPath); } } } - } -#endif -} -// Find the root of the Git repository, looking from the provided path and upward in its parent directories. -bool FindRootDirectory(const FString& InPath, FString& OutRepositoryRoot) -{ - OutRepositoryRoot = InPath; +#else + FString GitBinaryPath = TEXT("/usr/bin/git"); + bool bFound = CheckGitAvailability(GitBinaryPath); +#endif - auto TrimTrailing = [](FString& Str, const TCHAR Char) { - int32 Len = Str.Len(); - while (Len && Str[Len - 1] == Char) + if (bFound) { - Str = Str.LeftChop(1); - Len = Str.Len(); + FPaths::MakePlatformFilename(GitBinaryPath); + } + else + { + // If we did not find a path to Git, set it empty + GitBinaryPath.Empty(); } - }; - TrimTrailing(OutRepositoryRoot, '\\'); - TrimTrailing(OutRepositoryRoot, '/'); + return GitBinaryPath; + } - bool bFound = false; - FString PathToGitSubdirectory; - while (!bFound && !OutRepositoryRoot.IsEmpty()) + bool CheckGitAvailability(const FString& InPathToGitBinary, FGitVersion* OutVersion) { - // Look for the ".git" subdirectory (or file) present at the root of every Git repository - PathToGitSubdirectory = OutRepositoryRoot / TEXT(".git"); - bFound = IFileManager::Get().DirectoryExists(*PathToGitSubdirectory) || IFileManager::Get().FileExists(*PathToGitSubdirectory); - if (!bFound) + FString InfoMessages; + FString ErrorMessages; + bool bGitAvailable = RunCommandInternalRaw( + TEXT("version"), InPathToGitBinary, FString(), FGitSourceControlModule::GetEmptyStringArray(), + FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); + if (bGitAvailable) { - int32 LastSlashIndex; - if (OutRepositoryRoot.FindLastChar('/', LastSlashIndex)) + if (!InfoMessages.StartsWith("git version")) { - OutRepositoryRoot = OutRepositoryRoot.Left(LastSlashIndex); + bGitAvailable = false; } - else + else if (OutVersion) { - OutRepositoryRoot.Empty(); + ParseGitVersion(InfoMessages, OutVersion); } } - } - if (!bFound) - { - OutRepositoryRoot = InPath; // If not found, return the provided dir as best possible root. - } - return bFound; -} -void GetUserConfig(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutUserName, FString& OutUserEmail) -{ - bool bResults; - TArray InfoMessages; - TArray ErrorMessages; - TArray Parameters; - Parameters.Add(TEXT("user.name")); - bResults = RunCommandInternal(TEXT("config"), InPathToGitBinary, InRepositoryRoot, Parameters, FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); - if (bResults && InfoMessages.Num() > 0) - { - OutUserName = InfoMessages[0]; - } - else - { - OutUserName = TEXT(""); + return bGitAvailable; } - Parameters.Reset(1); - Parameters.Add(TEXT("user.email")); - InfoMessages.Reset(); - bResults &= RunCommandInternal(TEXT("config"), InPathToGitBinary, InRepositoryRoot, Parameters, FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); - if (bResults && InfoMessages.Num() > 0) - { - OutUserEmail = InfoMessages[0]; - } - else + void ParseGitVersion(const FString& InVersionString, FGitVersion* OutVersion) { - OutUserEmail = TEXT(""); +#if UE_BUILD_DEBUG + // Parse "git version 2.31.1.vfs.0.3" into the string "2.31.1.vfs.0.3" + const FString& TokenVersionStringPtr = InVersionString.RightChop(12); + if (!TokenVersionStringPtr.IsEmpty()) + { + // Parse the version into its numerical components + TArray ParsedVersionString; + TokenVersionStringPtr.ParseIntoArray(ParsedVersionString, TEXT(".")); + const int Num = ParsedVersionString.Num(); + if (Num >= 3) + { + if (ParsedVersionString[0].IsNumeric() && ParsedVersionString[1].IsNumeric() && + ParsedVersionString[2].IsNumeric()) + { + OutVersion->Major = FCString::Atoi(*ParsedVersionString[0]); + OutVersion->Minor = FCString::Atoi(*ParsedVersionString[1]); + OutVersion->Patch = FCString::Atoi(*ParsedVersionString[2]); + if (Num >= 5) + { + // If labeled with fork + if (!ParsedVersionString[3].IsNumeric()) + { + OutVersion->Fork = ParsedVersionString[3]; + OutVersion->bIsFork = true; + OutVersion->ForkMajor = FCString::Atoi(*ParsedVersionString[4]); + if (Num >= 6) + { + OutVersion->ForkMinor = FCString::Atoi(*ParsedVersionString[5]); + if (Num >= 7) + { + OutVersion->ForkPatch = FCString::Atoi(*ParsedVersionString[6]); + } + } + } + } + if (OutVersion->bIsFork) + { + UE_LOG(LogSourceControl, Log, TEXT("Git version %d.%d.%d.%s.%d.%d.%d"), OutVersion->Major, + OutVersion->Minor, OutVersion->Patch, *OutVersion->Fork, OutVersion->ForkMajor, + OutVersion->ForkMinor, OutVersion->ForkPatch); + } + else + { + UE_LOG(LogSourceControl, Log, TEXT("Git version %d.%d.%d"), OutVersion->Major, + OutVersion->Minor, OutVersion->Patch); + } + } + } + } +#endif } -} -bool GetBranchName(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutBranchName) -{ - const FGitSourceControlModule* GitSourceControl = FGitSourceControlModule::GetThreadSafe(); - if (!GitSourceControl) - { - return false; - } - const FGitSourceControlProvider& Provider = GitSourceControl->GetProvider(); - if (!Provider.GetBranchName().IsEmpty()) - { - OutBranchName = Provider.GetBranchName(); - return true; - } - - bool bResults; - TArray InfoMessages; - TArray ErrorMessages; - TArray Parameters; - Parameters.Add(TEXT("--short")); - Parameters.Add(TEXT("--quiet")); // no error message while in detached HEAD - Parameters.Add(TEXT("HEAD")); - bResults = RunCommand(TEXT("symbolic-ref"), InPathToGitBinary, InRepositoryRoot, Parameters, FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); - if (bResults && InfoMessages.Num() > 0) + // Find the root of the Git repository, looking from the provided path and upward in its parent directories. + bool FindRootDirectory(const FString& InPath, FString& OutRepositoryRoot) { - OutBranchName = InfoMessages[0]; + OutRepositoryRoot = InPath; + + auto TrimTrailing = [](FString& Str, const TCHAR Char) { + int32 Len = Str.Len(); + while (Len && Str[Len - 1] == Char) + { + Str = Str.LeftChop(1); + Len = Str.Len(); + } + }; + + TrimTrailing(OutRepositoryRoot, '\\'); + TrimTrailing(OutRepositoryRoot, '/'); + + bool bFound = false; + FString PathToGitSubdirectory; + while (!bFound && !OutRepositoryRoot.IsEmpty()) + { + // Look for the ".git" subdirectory (or file) present at the root of every Git repository + PathToGitSubdirectory = OutRepositoryRoot / TEXT(".git"); + bFound = IFileManager::Get().DirectoryExists(*PathToGitSubdirectory) || + IFileManager::Get().FileExists(*PathToGitSubdirectory); + if (!bFound) + { + int32 LastSlashIndex; + if (OutRepositoryRoot.FindLastChar('/', LastSlashIndex)) + { + OutRepositoryRoot = OutRepositoryRoot.Left(LastSlashIndex); + } + else + { + OutRepositoryRoot.Empty(); + } + } + } + if (!bFound) + { + OutRepositoryRoot = InPath; // If not found, return the provided dir as best possible root. + } + return bFound; } - else + + void GetUserConfig(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutUserName, + FString& OutUserEmail) { - Parameters.Reset(2); - Parameters.Add(TEXT("-1")); - Parameters.Add(TEXT("--format=\"%h\"")); // no error message while in detached HEAD - bResults = RunCommand(TEXT("log"), InPathToGitBinary, InRepositoryRoot, Parameters, FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); + bool bResults; + TArray InfoMessages; + TArray ErrorMessages; + TArray Parameters; + Parameters.Add(TEXT("user.name")); + bResults = RunCommandInternal(TEXT("config"), InPathToGitBinary, InRepositoryRoot, Parameters, + FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); if (bResults && InfoMessages.Num() > 0) { - OutBranchName = "HEAD detached at "; - OutBranchName += InfoMessages[0]; + OutUserName = InfoMessages[0]; } else { - bResults = false; + OutUserName = TEXT(""); } - } - - return bResults; -} - -bool GetRemoteBranchName(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutBranchName) -{ - const FGitSourceControlModule* GitSourceControl = FGitSourceControlModule::GetThreadSafe(); - if (!GitSourceControl) - { - return false; - } - const FGitSourceControlProvider& Provider = GitSourceControl->GetProvider(); - if (!Provider.GetRemoteBranchName().IsEmpty()) - { - OutBranchName = Provider.GetRemoteBranchName(); - return true; - } - TArray InfoMessages; - TArray ErrorMessages; - TArray Parameters; - Parameters.Add(TEXT("--abbrev-ref")); - Parameters.Add(TEXT("--symbolic-full-name")); - Parameters.Add(TEXT("@{u}")); - bool bResults = RunCommand(TEXT("rev-parse"), InPathToGitBinary, InRepositoryRoot, Parameters, FGitSourceControlModule::GetEmptyStringArray(), - InfoMessages, ErrorMessages); - if (bResults && InfoMessages.Num() > 0) - { - OutBranchName = InfoMessages[0]; - } - if (!bResults) - { - static bool bRunOnce = true; - if (bRunOnce) + Parameters.Reset(1); + Parameters.Add(TEXT("user.email")); + InfoMessages.Reset(); + bResults &= RunCommandInternal(TEXT("config"), InPathToGitBinary, InRepositoryRoot, Parameters, + FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); + if (bResults && InfoMessages.Num() > 0) + { + OutUserEmail = InfoMessages[0]; + } + else { - UE_LOG(LogSourceControl, Warning, TEXT("Upstream branch not found for the current branch, skipping current branch for remote check. Please push a remote branch.")); - bRunOnce = false; + OutUserEmail = TEXT(""); } } - return bResults; -} -bool GetRemoteBranchesWildcard(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& PatternMatch, TArray& OutBranchNames) -{ - TArray InfoMessages; - TArray ErrorMessages; - TArray Parameters; - Parameters.Add(TEXT("--remotes")); - Parameters.Add(TEXT("--list")); - bool bResults = RunCommand(TEXT("branch"), InPathToGitBinary, InRepositoryRoot, Parameters, { PatternMatch }, - InfoMessages, ErrorMessages); - if (bResults && InfoMessages.Num() > 0) + bool GetBranchName(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutBranchName) { - OutBranchNames = InfoMessages; - } - if (!bResults) - { - static bool bRunOnce = true; - if (bRunOnce) + const FGitSourceControlModule* GitSourceControl = FGitSourceControlModule::GetThreadSafe(); + if (!GitSourceControl) { - UE_LOG(LogSourceControl, Warning, TEXT("No remote branches matching pattern \"%s\" were found."), *PatternMatch); - bRunOnce = false; + return false; + } + const FGitSourceControlProvider& Provider = GitSourceControl->GetProvider(); + if (!Provider.GetBranchName().IsEmpty()) + { + OutBranchName = Provider.GetBranchName(); + return true; } - } - return bResults; -} - -bool GetCommitInfo(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutCommitId, FString& OutCommitSummary) -{ - bool bResults; - TArray InfoMessages; - TArray ErrorMessages; - TArray Parameters; - Parameters.Add(TEXT("-1")); - Parameters.Add(TEXT("--format=\"%H %s\"")); - bResults = RunCommandInternal(TEXT("log"), InPathToGitBinary, InRepositoryRoot, Parameters, FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); - if (bResults && InfoMessages.Num() > 0) - { - OutCommitId = InfoMessages[0].Left(40); - OutCommitSummary = InfoMessages[0].RightChop(41); - } - - return bResults; -} - -bool GetRemoteUrl(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutRemoteUrl) -{ - TArray InfoMessages; - TArray ErrorMessages; - TArray Parameters; - Parameters.Add(TEXT("get-url")); - Parameters.Add(TEXT("origin")); - const bool bResults = RunCommandInternal(TEXT("remote"), InPathToGitBinary, InRepositoryRoot, Parameters, FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); - if (bResults && InfoMessages.Num() > 0) - { - OutRemoteUrl = InfoMessages[0]; - } - - return bResults; -} - -bool RunCommand(const FString& InCommand, const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InParameters, - const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages) -{ - bool bResult = true; - if (InFiles.Num() > GitSourceControlConstants::MaxFilesPerBatch) - { - // Batch files up so we dont exceed command-line limits - int32 FileCount = 0; - while (FileCount < InFiles.Num()) + bool bResults; + TArray InfoMessages; + TArray ErrorMessages; + TArray Parameters; + Parameters.Add(TEXT("--short")); + Parameters.Add(TEXT("--quiet")); // no error message while in detached HEAD + Parameters.Add(TEXT("HEAD")); + bResults = RunCommand(TEXT("symbolic-ref"), InPathToGitBinary, InRepositoryRoot, Parameters, + FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); + if (bResults && InfoMessages.Num() > 0) { - TArray FilesInBatch; - for (int32 FileIndex = 0; FileCount < InFiles.Num() && FileIndex < GitSourceControlConstants::MaxFilesPerBatch; FileIndex++, FileCount++) + OutBranchName = InfoMessages[0]; + } + else + { + Parameters.Reset(2); + Parameters.Add(TEXT("-1")); + Parameters.Add(TEXT("--format=\"%h\"")); // no error message while in detached HEAD + bResults = RunCommand(TEXT("log"), InPathToGitBinary, InRepositoryRoot, Parameters, + FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); + if (bResults && InfoMessages.Num() > 0) { - FilesInBatch.Add(InFiles[FileCount]); + OutBranchName = "HEAD detached at "; + OutBranchName += InfoMessages[0]; + } + else + { + bResults = false; } - - TArray BatchResults; - TArray BatchErrors; - bResult &= RunCommandInternal(InCommand, InPathToGitBinary, InRepositoryRoot, InParameters, FilesInBatch, BatchResults, BatchErrors); - OutResults += BatchResults; - OutErrorMessages += BatchErrors; } - } - else - { - bResult = RunCommandInternal(InCommand, InPathToGitBinary, InRepositoryRoot, InParameters, InFiles, OutResults, OutErrorMessages); - } - - return bResult; -} - -#ifndef GIT_USE_CUSTOM_LFS -#define GIT_USE_CUSTOM_LFS 1 -#endif - -bool RunLFSCommand(const FString& InCommand, const FString& InRepositoryRoot, const FString& GitBinaryFallback, const TArray& InParameters, const TArray& InFiles, - TArray& OutResults, TArray& OutErrorMessages) -{ - FString Command = InCommand; -#if GIT_USE_CUSTOM_LFS - FString BaseDir = IPluginManager::Get().FindPlugin("GitSourceControl")->GetBaseDir(); -#if PLATFORM_WINDOWS - FString LFSLockBinary = FString::Printf(TEXT("%s/git-lfs.exe"), *BaseDir); -#elif PLATFORM_MAC -#if ENGINE_MAJOR_VERSION >= 5 -#if PLATFORM_MAC_ARM64 - FString LFSLockBinary = FString::Printf(TEXT("%s/git-lfs-mac-arm64"), *BaseDir); -#else - FString LFSLockBinary = FString::Printf(TEXT("%s/git-lfs-mac-amd64"), *BaseDir); -#endif -#else - FString LFSLockBinary = FString::Printf(TEXT("%s/git-lfs-mac-amd64"), *BaseDir); -#endif -#elif PLATFORM_LINUX - FString LFSLockBinary = FString::Printf(TEXT("%s/git-lfs"), *BaseDir); -#else - ensureMsgf(false, TEXT("Unhandled platform for LFS binary!")); - const FString& LFSLockBinary = GitBinaryFallback; - Command = TEXT("lfs ") + Command; -#endif -#else - const FString& LFSLockBinary = GitBinaryFallback; - Command = TEXT("lfs ") + Command; -#endif - - return GitSourceControlUtils::RunCommand(Command, LFSLockBinary, InRepositoryRoot, InParameters, InFiles, OutResults, OutErrorMessages); -} - -// Run a Git "commit" command by batches -bool RunCommit(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InParameters, const TArray& InFiles, - TArray& OutResults, TArray& OutErrorMessages) -{ - bool bResult = true; - TArray AddParameters{TEXT("-A")}; + return bResults; + } - if (InFiles.Num() > GitSourceControlConstants::MaxFilesPerBatch) + bool GetRemoteBranchName(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutBranchName) { - // Batch files up so we dont exceed command-line limits - int32 FileCount = 0; + const FGitSourceControlModule* GitSourceControl = FGitSourceControlModule::GetThreadSafe(); + if (!GitSourceControl) { - TArray FilesInBatch; - for (int32 FileIndex = 0; FileIndex < GitSourceControlConstants::MaxFilesPerBatch; FileIndex++, FileCount++) - { - FilesInBatch.Add(InFiles[FileCount]); - } - bResult &= RunCommandInternal(TEXT("add"), InPathToGitBinary, InRepositoryRoot, AddParameters, FilesInBatch, OutResults, OutErrorMessages); - // First batch is a simple "git commit" command with only the first files - bResult &= RunCommandInternal(TEXT("commit"), InPathToGitBinary, InRepositoryRoot, InParameters, FilesInBatch, OutResults, OutErrorMessages); + return false; + } + const FGitSourceControlProvider& Provider = GitSourceControl->GetProvider(); + if (!Provider.GetRemoteBranchName().IsEmpty()) + { + OutBranchName = Provider.GetRemoteBranchName(); + return true; } + TArray InfoMessages; + TArray ErrorMessages; TArray Parameters; - for (const auto& Parameter : InParameters) + Parameters.Add(TEXT("--abbrev-ref")); + Parameters.Add(TEXT("--symbolic-full-name")); + Parameters.Add(TEXT("@{u}")); + bool bResults = RunCommand(TEXT("rev-parse"), InPathToGitBinary, InRepositoryRoot, Parameters, + FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); + if (bResults && InfoMessages.Num() > 0) { - Parameters.Add(Parameter); + OutBranchName = InfoMessages[0]; } - Parameters.Add(TEXT("--amend")); - - while (FileCount < InFiles.Num()) + if (!bResults) { - TArray FilesInBatch; - for (int32 FileIndex = 0; FileCount < InFiles.Num() && FileIndex < GitSourceControlConstants::MaxFilesPerBatch; FileIndex++, FileCount++) + static bool bRunOnce = true; + if (bRunOnce) { - FilesInBatch.Add(InFiles[FileCount]); + UE_LOG(LogSourceControl, Warning, + TEXT("Upstream branch not found for the current branch, skipping current branch for remote " + "check. Please push a remote branch.")); + bRunOnce = false; } - // Next batches "amend" the commit with some more files - TArray BatchResults; - TArray BatchErrors; - bResult &= RunCommandInternal(TEXT("add"), InPathToGitBinary, InRepositoryRoot, AddParameters, FilesInBatch, OutResults, OutErrorMessages); - bResult &= RunCommandInternal(TEXT("commit"), InPathToGitBinary, InRepositoryRoot, Parameters, FilesInBatch, BatchResults, BatchErrors); - OutResults += BatchResults; - OutErrorMessages += BatchErrors; } - } - else - { - bResult &= RunCommandInternal(TEXT("add"), InPathToGitBinary, InRepositoryRoot, AddParameters, InFiles, OutResults, OutErrorMessages); - bResult = RunCommandInternal(TEXT("commit"), InPathToGitBinary, InRepositoryRoot, InParameters, InFiles, OutResults, OutErrorMessages); + return bResults; } - return bResult; -} - -/** - * Parse informations on a file locked with Git LFS - * - * Examples output of "git lfs locks": -Content\ThirdPersonBP\Blueprints\ThirdPersonCharacter.uasset SRombauts ID:891 -Content\ThirdPersonBP\Blueprints\ThirdPersonCharacter.uasset ID:891 -Content\ThirdPersonBP\Blueprints\ThirdPersonCharacter.uasset ID:891 - */ -class FGitLfsLocksParser -{ -public: - FGitLfsLocksParser(const FString& InRepositoryRoot, const FString& InStatus, const bool bAbsolutePaths = true) + bool GetRemoteBranchesWildcard(const FString& InPathToGitBinary, const FString& InRepositoryRoot, + const FString& PatternMatch, TArray& OutBranchNames) { - TArray Informations; - InStatus.ParseIntoArray(Informations, TEXT("\t"), true); - - if (Informations.Num() >= 2) - { - Informations[0].TrimEndInline(); // Trim whitespace from the end of the filename - Informations[1].TrimEndInline(); // Trim whitespace from the end of the username - if (bAbsolutePaths) - LocalFilename = FPaths::ConvertRelativePathToFull(InRepositoryRoot, Informations[0]); - else - LocalFilename = Informations[0]; - // Filename ID (or we expect it to be the username, but it's empty, or is the ID, we have to assume it's the current user) - if (Informations.Num() == 2 || Informations[1].IsEmpty() || Informations[1].StartsWith(TEXT("ID:"))) - { - // TODO: thread safety - LockUser = FGitSourceControlModule::Get().GetProvider().GetLockUser(); - } - // Filename Username ID - else + TArray InfoMessages; + TArray ErrorMessages; + TArray Parameters; + Parameters.Add(TEXT("--remotes")); + Parameters.Add(TEXT("--list")); + bool bResults = RunCommand(TEXT("branch"), InPathToGitBinary, InRepositoryRoot, Parameters, {PatternMatch}, + InfoMessages, ErrorMessages); + if (bResults && InfoMessages.Num() > 0) + { + OutBranchNames = InfoMessages; + } + if (!bResults) + { + static bool bRunOnce = true; + if (bRunOnce) { - LockUser = MoveTemp(Informations[1]); + UE_LOG(LogSourceControl, Warning, TEXT("No remote branches matching pattern \"%s\" were found."), + *PatternMatch); + bRunOnce = false; } } + return bResults; } - // Filename on disk - FString LocalFilename; - // Name of user who has file locked - FString LockUser; -}; - -/** - * @brief Extract the relative filename from a Git status result. - * - * Examples of status results: -M Content/Textures/T_Perlin_Noise_M.uasset -R Content/Textures/T_Perlin_Noise_M.uasset -> Content/Textures/T_Perlin_Noise_M2.uasset -?? Content/Materials/M_Basic_Wall.uasset -!! BasicCode.sln - * - * @param[in] InResult One line of status - * @return Relative filename extracted from the line of status - * - * @see FGitStatusFileMatcher and StateFromGitStatus() - */ -static FString FilenameFromGitStatus(const FString& InResult) -{ - int32 RenameIndex; - if (InResult.FindLastChar('>', RenameIndex)) - { - // Extract only the second part of a rename "from -> to" - return InResult.RightChop(RenameIndex + 2); - } - else + bool GetCommitInfo(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutCommitId, + FString& OutCommitSummary) { - // Extract the relative filename from the Git status result (after the 2 letters status and 1 space) - return InResult.RightChop(3); - } -} + bool bResults; + TArray InfoMessages; + TArray ErrorMessages; + TArray Parameters; + Parameters.Add(TEXT("-1")); + Parameters.Add(TEXT("--format=\"%H %s\"")); + bResults = RunCommandInternal(TEXT("log"), InPathToGitBinary, InRepositoryRoot, Parameters, + FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); + if (bResults && InfoMessages.Num() > 0) + { + OutCommitId = InfoMessages[0].Left(40); + OutCommitSummary = InfoMessages[0].RightChop(41); + } -/** Match the relative filename of a Git status result with a provided absolute filename */ -class FGitStatusFileMatcher -{ -public: - FGitStatusFileMatcher(const FString& InAbsoluteFilename) : AbsoluteFilename(InAbsoluteFilename) - {} + return bResults; + } - bool operator()(const FString& InResult) const + bool GetRemoteUrl(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutRemoteUrl) { - return AbsoluteFilename.Contains(FilenameFromGitStatus(InResult)); + TArray InfoMessages; + TArray ErrorMessages; + TArray Parameters; + Parameters.Add(TEXT("get-url")); + Parameters.Add(TEXT("origin")); + const bool bResults = + RunCommandInternal(TEXT("remote"), InPathToGitBinary, InRepositoryRoot, Parameters, + FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); + if (bResults && InfoMessages.Num() > 0) + { + OutRemoteUrl = InfoMessages[0]; + } + + return bResults; } -private: - const FString& AbsoluteFilename; -}; - -/** - * Extract and interpret the file state from the given Git status result. - * @see http://git-scm.com/docs/git-status - * ' ' = unmodified - * 'M' = modified - * 'A' = added - * 'D' = deleted - * 'R' = renamed - * 'C' = copied - * 'U' = updated but unmerged - * '?' = unknown/untracked - * '!' = ignored - */ -class FGitStatusParser -{ -public: - FGitStatusParser(const FString& InResult) + bool RunCommand(const FString& InCommand, const FString& InPathToGitBinary, const FString& InRepositoryRoot, + const TArray& InParameters, const TArray& InFiles, TArray& OutResults, + TArray& OutErrorMessages) { - TCHAR IndexState = InResult[0]; - TCHAR WCopyState = InResult[1]; - if ((IndexState == 'U' || WCopyState == 'U') || (IndexState == 'A' && WCopyState == 'A') || (IndexState == 'D' && WCopyState == 'D')) - { - // "Unmerged" conflict cases are generally marked with a "U", - // but there are also the special cases of both "A"dded, or both "D"eleted - FileState = EFileState::Unmerged; - TreeState = ETreeState::Working; - return; - } + bool bResult = true; - if (IndexState == ' ') - { - TreeState = ETreeState::Working; - } - else if (WCopyState == ' ') + if (InFiles.Num() > GitSourceControlConstants::MaxFilesPerBatch) { - TreeState = ETreeState::Staged; - } + // Batch files up so we dont exceed command-line limits + int32 FileCount = 0; + while (FileCount < InFiles.Num()) + { + TArray FilesInBatch; + for (int32 FileIndex = 0; + FileCount < InFiles.Num() && FileIndex < GitSourceControlConstants::MaxFilesPerBatch; + FileIndex++, FileCount++) + { + FilesInBatch.Add(InFiles[FileCount]); + } - if (IndexState == '?' || WCopyState == '?') - { - TreeState = ETreeState::Untracked; - FileState = EFileState::Unknown; - } - else if (IndexState == '!' || WCopyState == '!') - { - TreeState = ETreeState::Ignored; - FileState = EFileState::Unknown; - } - else if (IndexState == 'A') - { - FileState = EFileState::Added; + TArray BatchResults; + TArray BatchErrors; + bResult &= RunCommandInternal(InCommand, InPathToGitBinary, InRepositoryRoot, InParameters, + FilesInBatch, BatchResults, BatchErrors); + OutResults += BatchResults; + OutErrorMessages += BatchErrors; + } } - else if (IndexState == 'D') + else { - FileState = EFileState::Deleted; + bResult = RunCommandInternal(InCommand, InPathToGitBinary, InRepositoryRoot, InParameters, InFiles, + OutResults, OutErrorMessages); } - else if (WCopyState == 'D') + + return bResult; + } + +#ifndef GIT_USE_CUSTOM_LFS + #define GIT_USE_CUSTOM_LFS 1 +#endif + + // Run a Git "commit" command by batches + bool RunCommit(const FString& InPathToGitBinary, const FString& InRepositoryRoot, + const TArray& InParameters, const TArray& InFiles, TArray& OutResults, + TArray& OutErrorMessages) + { + bool bResult = true; + + TArray AddParameters {TEXT("-A")}; + + if (InFiles.Num() > GitSourceControlConstants::MaxFilesPerBatch) { - FileState = EFileState::Missing; + // Batch files up so we dont exceed command-line limits + int32 FileCount = 0; + { + TArray FilesInBatch; + for (int32 FileIndex = 0; FileIndex < GitSourceControlConstants::MaxFilesPerBatch; + FileIndex++, FileCount++) + { + FilesInBatch.Add(InFiles[FileCount]); + } + bResult &= RunCommandInternal(TEXT("add"), InPathToGitBinary, InRepositoryRoot, AddParameters, + FilesInBatch, OutResults, OutErrorMessages); + // First batch is a simple "git commit" command with only the first files + bResult &= RunCommandInternal(TEXT("commit"), InPathToGitBinary, InRepositoryRoot, InParameters, + FilesInBatch, OutResults, OutErrorMessages); + } + + TArray Parameters; + for (const auto& Parameter : InParameters) + { + Parameters.Add(Parameter); + } + Parameters.Add(TEXT("--amend")); + + while (FileCount < InFiles.Num()) + { + TArray FilesInBatch; + for (int32 FileIndex = 0; + FileCount < InFiles.Num() && FileIndex < GitSourceControlConstants::MaxFilesPerBatch; + FileIndex++, FileCount++) + { + FilesInBatch.Add(InFiles[FileCount]); + } + // Next batches "amend" the commit with some more files + TArray BatchResults; + TArray BatchErrors; + bResult &= RunCommandInternal(TEXT("add"), InPathToGitBinary, InRepositoryRoot, AddParameters, + FilesInBatch, OutResults, OutErrorMessages); + bResult &= RunCommandInternal(TEXT("commit"), InPathToGitBinary, InRepositoryRoot, Parameters, + FilesInBatch, BatchResults, BatchErrors); + OutResults += BatchResults; + OutErrorMessages += BatchErrors; + } } - else if (IndexState == 'M' || WCopyState == 'M') + else { - FileState = EFileState::Modified; + bResult &= RunCommandInternal(TEXT("add"), InPathToGitBinary, InRepositoryRoot, AddParameters, InFiles, + OutResults, OutErrorMessages); + bResult = RunCommandInternal(TEXT("commit"), InPathToGitBinary, InRepositoryRoot, InParameters, InFiles, + OutResults, OutErrorMessages); } - else if (IndexState == 'R') + + return bResult; + } + + /** + * Parse informations on a file locked with Git LFS + * + * Examples output of "git lfs locks": + Content\ThirdPersonBP\Blueprints\ThirdPersonCharacter.uasset SRombauts ID:891 + Content\ThirdPersonBP\Blueprints\ThirdPersonCharacter.uasset ID:891 + Content\ThirdPersonBP\Blueprints\ThirdPersonCharacter.uasset ID:891 + */ + class FGitLfsLocksParser + { + public: + FGitLfsLocksParser(const FString& InRepositoryRoot, const FString& InStatus, const bool bAbsolutePaths = true) { - FileState = EFileState::Renamed; + TArray Informations; + InStatus.ParseIntoArray(Informations, TEXT("\t"), true); + + if (Informations.Num() >= 2) + { + Informations[0].TrimEndInline(); // Trim whitespace from the end of the filename + Informations[1].TrimEndInline(); // Trim whitespace from the end of the username + if (bAbsolutePaths) + LocalFilename = FPaths::ConvertRelativePathToFull(InRepositoryRoot, Informations[0]); + else + LocalFilename = Informations[0]; + // Filename ID (or we expect it to be the username, but it's empty, or is the ID, we have to assume it's + // the current user) + if (Informations.Num() == 2 || Informations[1].IsEmpty() || Informations[1].StartsWith(TEXT("ID:"))) + { + // TODO: thread safety + LockUser = FGitSourceControlModule::Get().GetProvider().GetLockUser(); + } + // Filename Username ID + else + { + LockUser = MoveTemp(Informations[1]); + } + } } - else if (IndexState == 'C') - { - FileState = EFileState::Copied; + + // Filename on disk + FString LocalFilename; + // Name of user who has file locked + FString LockUser; + }; + + /** + * @brief Extract the relative filename from a Git status result. + * + * Examples of status results: + M Content/Textures/T_Perlin_Noise_M.uasset + R Content/Textures/T_Perlin_Noise_M.uasset -> Content/Textures/T_Perlin_Noise_M2.uasset + ?? Content/Materials/M_Basic_Wall.uasset + !! BasicCode.sln + * + * @param[in] InResult One line of status + * @return Relative filename extracted from the line of status + * + * @see FGitStatusFileMatcher and StateFromGitStatus() + */ + static FString FilenameFromGitStatus(const FString& InResult) + { + int32 RenameIndex; + if (InResult.FindLastChar('>', RenameIndex)) + { + // Extract only the second part of a rename "from -> to" + return InResult.RightChop(RenameIndex + 2); } else { - // Unmodified never yield a status - FileState = EFileState::Unknown; + // Extract the relative filename from the Git status result (after the 2 letters status and 1 space) + return InResult.RightChop(3); } } - EFileState::Type FileState; - ETreeState::Type TreeState; -}; - -/** - * Extract the status of a unmerged (conflict) file - * - * Example output of git ls-files --unmerged Content/Blueprints/BP_Test.uasset -100644 d9b33098273547b57c0af314136f35b494e16dcb 1 Content/Blueprints/BP_Test.uasset -100644 a14347dc3b589b78fb19ba62a7e3982f343718bc 2 Content/Blueprints/BP_Test.uasset -100644 f3137a7167c840847cd7bd2bf07eefbfb2d9bcd2 3 Content/Blueprints/BP_Test.uasset - * - * 1: The "common ancestor" of the file (the version of the file that both the current and other branch originated from). - * 2: The version from the current branch (the master branch in this case). - * 3: The version from the other branch (the test branch) -*/ -class FGitConflictStatusParser -{ -public: - /** Parse the unmerge status: extract the base SHA1 identifier of the file */ - FGitConflictStatusParser(const TArray& InResults) + /** Match the relative filename of a Git status result with a provided absolute filename */ + class FGitStatusFileMatcher { - const FString& CommonAncestor = InResults[0]; // 1: The common ancestor of merged branches - CommonAncestorFileId = CommonAncestor.Mid(7, 40); -#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 - CommonAncestorFileId = CommonAncestor.Mid(7, 40); - CommonAncestorFilename = CommonAncestor.Right(50); + public: + FGitStatusFileMatcher(const FString& InAbsoluteFilename) : AbsoluteFilename(InAbsoluteFilename) {} - if (ensure(InResults.IsValidIndex(2))) + bool operator()(const FString& InResult) const { - const FString& RemoteBranch = InResults[2]; // 1: The common ancestor of merged branches - RemoteFileId = RemoteBranch.Mid(7, 40); - RemoteFilename = RemoteBranch.Right(50); + return AbsoluteFilename.Contains(FilenameFromGitStatus(InResult)); } + + private: + const FString& AbsoluteFilename; + }; + + /** + * Extract and interpret the file state from the given Git status result. + * @see http://git-scm.com/docs/git-status + * ' ' = unmodified + * 'M' = modified + * 'A' = added + * 'D' = deleted + * 'R' = renamed + * 'C' = copied + * 'U' = updated but unmerged + * '?' = unknown/untracked + * '!' = ignored + */ + class FGitStatusParser + { + public: + FGitStatusParser(const FString& InResult) + { + TCHAR IndexState = InResult[0]; + TCHAR WCopyState = InResult[1]; + if ((IndexState == 'U' || WCopyState == 'U') || (IndexState == 'A' && WCopyState == 'A') || + (IndexState == 'D' && WCopyState == 'D')) + { + // "Unmerged" conflict cases are generally marked with a "U", + // but there are also the special cases of both "A"dded, or both "D"eleted + FileState = EFileState::Unmerged; + TreeState = ETreeState::Working; + return; + } + + if (IndexState == ' ') + { + TreeState = ETreeState::Working; + } + else if (WCopyState == ' ') + { + TreeState = ETreeState::Staged; + } + + if (IndexState == '?' || WCopyState == '?') + { + TreeState = ETreeState::Untracked; + FileState = EFileState::Unknown; + } + else if (IndexState == '!' || WCopyState == '!') + { + TreeState = ETreeState::Ignored; + FileState = EFileState::Unknown; + } + else if (IndexState == 'A') + { + FileState = EFileState::Added; + } + else if (IndexState == 'D') + { + FileState = EFileState::Deleted; + } + else if (WCopyState == 'D') + { + FileState = EFileState::Missing; + } + else if (IndexState == 'M' || WCopyState == 'M') + { + FileState = EFileState::Modified; + } + else if (IndexState == 'R') + { + FileState = EFileState::Renamed; + } + else if (IndexState == 'C') + { + FileState = EFileState::Copied; + } + else + { + // Unmodified never yield a status + FileState = EFileState::Unknown; + } + } + + EFileState::Type FileState; + ETreeState::Type TreeState; + }; + + /** + * Extract the status of a unmerged (conflict) file + * + * Example output of git ls-files --unmerged Content/Blueprints/BP_Test.uasset + 100644 d9b33098273547b57c0af314136f35b494e16dcb 1 Content/Blueprints/BP_Test.uasset + 100644 a14347dc3b589b78fb19ba62a7e3982f343718bc 2 Content/Blueprints/BP_Test.uasset + 100644 f3137a7167c840847cd7bd2bf07eefbfb2d9bcd2 3 Content/Blueprints/BP_Test.uasset + * + * 1: The "common ancestor" of the file (the version of the file that both the current and other branch originated + from). + * 2: The version from the current branch (the master branch in this case). + * 3: The version from the other branch (the test branch) + */ + class FGitConflictStatusParser + { + public: + /** Parse the unmerge status: extract the base SHA1 identifier of the file */ + FGitConflictStatusParser(const TArray& InResults) + { + const FString& CommonAncestor = InResults[0]; // 1: The common ancestor of merged branches + CommonAncestorFileId = CommonAncestor.Mid(7, 40); +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + CommonAncestorFileId = CommonAncestor.Mid(7, 40); + CommonAncestorFilename = CommonAncestor.Right(50); + + if (ensure(InResults.IsValidIndex(2))) + { + const FString& RemoteBranch = InResults[2]; // 1: The common ancestor of merged branches + RemoteFileId = RemoteBranch.Mid(7, 40); + RemoteFilename = RemoteBranch.Right(50); + } #endif - } + } - FString CommonAncestorFileId; ///< SHA1 Id of the file (warning: not the commit Id) + FString CommonAncestorFileId; ///< SHA1 Id of the file (warning: not the commit Id) #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 - FString RemoteFileId; ///< SHA1 Id of the file (warning: not the commit Id) + FString RemoteFileId; ///< SHA1 Id of the file (warning: not the commit Id) - FString CommonAncestorFilename; - FString RemoteFilename; + FString CommonAncestorFilename; + FString RemoteFilename; #endif -}; + }; -/** Execute a command to get the details of a conflict */ -static void RunGetConflictStatus(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InFile, FGitSourceControlState& InOutFileState) -{ - TArray ErrorMessages; - TArray Results; - TArray Files; - Files.Add(InFile); - TArray Parameters; - Parameters.Add(TEXT("--unmerged")); - bool bResult = RunCommandInternal(TEXT("ls-files"), InPathToGitBinary, InRepositoryRoot, Parameters, Files, Results, ErrorMessages); - if (bResult && Results.Num() == 3) + /** Execute a command to get the details of a conflict */ + static void RunGetConflictStatus(const FString& InPathToGitBinary, const FString& InRepositoryRoot, + const FString& InFile, FGitSourceControlState& InOutFileState) { - // Parse the unmerge status: extract the base revision (or the other branch?) - FGitConflictStatusParser ConflictStatus(Results); + TArray ErrorMessages; + TArray Results; + TArray Files; + Files.Add(InFile); + TArray Parameters; + Parameters.Add(TEXT("--unmerged")); + bool bResult = RunCommandInternal(TEXT("ls-files"), InPathToGitBinary, InRepositoryRoot, Parameters, Files, + Results, ErrorMessages); + if (bResult && Results.Num() == 3) + { + // Parse the unmerge status: extract the base revision (or the other branch?) + FGitConflictStatusParser ConflictStatus(Results); #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 - InOutFileState.PendingResolveInfo.BaseFile = ConflictStatus.CommonAncestorFilename; - InOutFileState.PendingResolveInfo.BaseRevision = ConflictStatus.CommonAncestorFileId; - InOutFileState.PendingResolveInfo.RemoteFile = ConflictStatus.RemoteFilename; - InOutFileState.PendingResolveInfo.RemoteRevision = ConflictStatus.RemoteFileId; + InOutFileState.PendingResolveInfo.BaseFile = ConflictStatus.CommonAncestorFilename; + InOutFileState.PendingResolveInfo.BaseRevision = ConflictStatus.CommonAncestorFileId; + InOutFileState.PendingResolveInfo.RemoteFile = ConflictStatus.RemoteFilename; + InOutFileState.PendingResolveInfo.RemoteRevision = ConflictStatus.RemoteFileId; #else - InOutFileState.PendingMergeBaseFileHash = ConflictStatus.CommonAncestorFileId; + InOutFileState.PendingMergeBaseFileHash = ConflictStatus.CommonAncestorFileId; #endif + } } -} -TArray UnlinkPackages(const TArray& InPackageNames) -{ - TArray LoadedPackages; - // UE-COPY: ContentBrowserUtils::SyncPathsFromSourceControl() - if (InPackageNames.Num() > 0) + TArray UnlinkPackages(const TArray& InPackageNames) { - TArray PackagesToUnlink; - for (const auto& Filename : InPackageNames) + TArray LoadedPackages; + // UE-COPY: ContentBrowserUtils::SyncPathsFromSourceControl() + if (InPackageNames.Num() > 0) { - FString PackageName; - if (FPackageName::TryConvertFilenameToLongPackageName(Filename, PackageName)) + TArray PackagesToUnlink; + for (const auto& Filename : InPackageNames) { - PackagesToUnlink.Add(*PackageName); + FString PackageName; + if (FPackageName::TryConvertFilenameToLongPackageName(Filename, PackageName)) + { + PackagesToUnlink.Add(*PackageName); + } } - } - // Form a list of loaded packages to reload... - LoadedPackages.Reserve(PackagesToUnlink.Num()); - for (const FString& PackageName : PackagesToUnlink) - { - UPackage* Package = FindPackage(nullptr, *PackageName); - if (Package) + // Form a list of loaded packages to reload... + LoadedPackages.Reserve(PackagesToUnlink.Num()); + for (const FString& PackageName : PackagesToUnlink) { - LoadedPackages.Emplace(Package); - - // Detach the linkers of any loaded packages so that SCC can overwrite the files... - if (!Package->IsFullyLoaded()) + UPackage* Package = FindPackage(nullptr, *PackageName); + if (Package) { - FlushAsyncLoading(); - Package->FullyLoad(); + LoadedPackages.Emplace(Package); + + // Detach the linkers of any loaded packages so that SCC can overwrite the files... + if (!Package->IsFullyLoaded()) + { + FlushAsyncLoading(); + Package->FullyLoad(); + } + ResetLoaders(Package); } - ResetLoaders(Package); } } + return LoadedPackages; } - return LoadedPackages; -} -void ReloadPackages(TArray& InPackagesToReload) -{ - // UE-COPY: ContentBrowserUtils::SyncPathsFromSourceControl() - // Syncing may have deleted some packages, so we need to unload those rather than re-load them... - TArray PackagesToUnload; - InPackagesToReload.RemoveAll([&](UPackage* InPackage) -> bool { - const FString PackageExtension = InPackage->ContainsMap() ? FPackageName::GetMapPackageExtension() : FPackageName::GetAssetPackageExtension(); - const FString PackageFilename = FPackageName::LongPackageNameToFilename(InPackage->GetName(), PackageExtension); - if (!FPaths::FileExists(PackageFilename)) - { - PackagesToUnload.Emplace(InPackage); - return true; // remove package - } - return false; // keep package - }); + void ReloadPackages(TArray& InPackagesToReload) + { + // UE-COPY: ContentBrowserUtils::SyncPathsFromSourceControl() + // Syncing may have deleted some packages, so we need to unload those rather than re-load them... + TArray PackagesToUnload; + InPackagesToReload.RemoveAll([&](UPackage* InPackage) -> bool { + const FString PackageExtension = InPackage->ContainsMap() ? FPackageName::GetMapPackageExtension() + : FPackageName::GetAssetPackageExtension(); + const FString PackageFilename = + FPackageName::LongPackageNameToFilename(InPackage->GetName(), PackageExtension); + if (!FPaths::FileExists(PackageFilename)) + { + PackagesToUnload.Emplace(InPackage); + return true; // remove package + } + return false; // keep package + }); - // Hot-reload the new packages... - UPackageTools::ReloadPackages(InPackagesToReload); + // Hot-reload the new packages... + UPackageTools::ReloadPackages(InPackagesToReload); - // Unload any deleted packages... - UPackageTools::UnloadPackages(PackagesToUnload); -} + // Unload any deleted packages... + UPackageTools::UnloadPackages(PackagesToUnload); + } -/// Convert filename relative to the repository root to absolute path (inplace) -void AbsoluteFilenames(const FString& InRepositoryRoot, TArray& InFileNames) -{ - for (auto& FileName : InFileNames) + /// Convert filename relative to the repository root to absolute path (inplace) + void AbsoluteFilenames(const FString& InRepositoryRoot, TArray& InFileNames) { - FileName = FPaths::ConvertRelativePathToFull(InRepositoryRoot, FileName); + for (auto& FileName : InFileNames) + { + FileName = FPaths::ConvertRelativePathToFull(InRepositoryRoot, FileName); + } } -} -/** Run a 'git ls-files' command to get all files tracked by Git recursively in a directory. - * - * Called in case of a "directory status" (no file listed in the command) when using the "Submit to Revision Control" menu. - */ -bool ListFilesInDirectoryRecurse(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InDirectory, TArray& OutFiles) -{ - TArray ErrorMessages; - TArray Directory; - Directory.Add(InDirectory); - const bool bResult = RunCommandInternal(TEXT("ls-files"), InPathToGitBinary, InRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), Directory, OutFiles, ErrorMessages); - AbsoluteFilenames(InRepositoryRoot, OutFiles); - return bResult; -} + /** Run a 'git ls-files' command to get all files tracked by Git recursively in a directory. + * + * Called in case of a "directory status" (no file listed in the command) when using the "Submit to Revision + * Control" menu. + */ + bool ListFilesInDirectoryRecurse(const FString& InPathToGitBinary, const FString& InRepositoryRoot, + const FString& InDirectory, TArray& OutFiles) + { + TArray ErrorMessages; + TArray Directory; + Directory.Add(InDirectory); + const bool bResult = + RunCommandInternal(TEXT("ls-files"), InPathToGitBinary, InRepositoryRoot, + FGitSourceControlModule::GetEmptyStringArray(), Directory, OutFiles, ErrorMessages); + AbsoluteFilenames(InRepositoryRoot, OutFiles); + return bResult; + } -/** Parse the array of strings results of a 'git status' command for a directory - * - * Called in case of a "directory status" (no file listed in the command) ONLY to detect Deleted/Missing/Untracked files - * since those files are not listed by the 'git ls-files' command. - * - * @see #ParseFileStatusResult() above for an example of a 'git status' results - */ -static void ParseDirectoryStatusResult(const bool InUsingLfsLocking, const TMap& InResults, TMap& OutStates) -{ - // Iterate on each line of result of the status command - for (const auto& Result : InResults) + /** Parse the array of strings results of a 'git status' command for a directory + * + * Called in case of a "directory status" (no file listed in the command) ONLY to detect Deleted/Missing/Untracked + * files since those files are not listed by the 'git ls-files' command. + * + * @see #ParseFileStatusResult() above for an example of a 'git status' results + */ + static void ParseDirectoryStatusResult(const bool InUsingLfsLocking, const TMap& InResults, + TMap& OutStates) { - FGitSourceControlState FileState(Result.Key); - if (!InUsingLfsLocking) - { - FileState.State.LockState = ELockState::Unlockable; - } - FGitStatusParser StatusParser(Result.Value); - if ((EFileState::Deleted == StatusParser.FileState) || (EFileState::Missing == StatusParser.FileState) || (ETreeState::Untracked == StatusParser.TreeState)) + // Iterate on each line of result of the status command + for (const auto& Result : InResults) { - FileState.State.FileState = StatusParser.FileState; - FileState.State.TreeState = StatusParser.TreeState; - OutStates.Add(Result.Key, MoveTemp(FileState)); + FGitSourceControlState FileState(Result.Key); + if (!InUsingLfsLocking) + { + FileState.State.LockState = ELockState::Unlockable; + } + FGitStatusParser StatusParser(Result.Value); + if ((EFileState::Deleted == StatusParser.FileState) || (EFileState::Missing == StatusParser.FileState) || + (ETreeState::Untracked == StatusParser.TreeState)) + { + FileState.State.FileState = StatusParser.FileState; + FileState.State.TreeState = StatusParser.TreeState; + OutStates.Add(Result.Key, MoveTemp(FileState)); + } } } -} -/** Parse the array of strings results of a 'git status' command for a provided list of files all in a common directory - * - * Called in case of a normal refresh of status on a list of assets in a the Content Browser (or user selected "Refresh" context menu). - * - * Example git status results: -M Content/Textures/T_Perlin_Noise_M.uasset -R Content/Textures/T_Perlin_Noise_M.uasset -> Content/Textures/T_Perlin_Noise_M2.uasset -?? Content/Materials/M_Basic_Wall.uasset -!! BasicCode.sln -*/ -static void ParseFileStatusResult(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool InUsingLfsLocking, const TSet& InFiles, - const TMap& InResults, TMap& OutStates) -{ - FGitSourceControlModule* GitSourceControl = FGitSourceControlModule::GetThreadSafe(); - if (!GitSourceControl) + /** Parse the array of strings results of a 'git status' command for a provided list of files all in a common + directory + * + * Called in case of a normal refresh of status on a list of assets in a the Content Browser (or user selected + "Refresh" context menu). + * + * Example git status results: + M Content/Textures/T_Perlin_Noise_M.uasset + R Content/Textures/T_Perlin_Noise_M.uasset -> Content/Textures/T_Perlin_Noise_M2.uasset + ?? Content/Materials/M_Basic_Wall.uasset + !! BasicCode.sln + */ + static void ParseFileStatusResult(const FString& InPathToGitBinary, const FString& InRepositoryRoot, + const bool InUsingLfsLocking, const TSet& InFiles, + const TMap& InResults, + TMap& OutStates) { - return; - } - FGitSourceControlProvider& Provider = GitSourceControl->GetProvider(); - const FString& LfsUserName = Provider.GetLockUser(); + FGitSourceControlModule* GitSourceControl = FGitSourceControlModule::GetThreadSafe(); + if (!GitSourceControl) + { + return; + } + FGitSourceControlProvider& Provider = GitSourceControl->GetProvider(); + const FString& LfsUserName = Provider.GetLockUser(); - TMap LockedFiles; - TMap Results = InResults; - bool bCheckedLockedFiles = false; + TMap LockedFiles; + TMap Results = InResults; + bool bCheckedLockedFiles = false; - FString Result; + FString Result; - // Iterate on all files explicitly listed in the command - for (const auto& File : InFiles) - { - FGitSourceControlState FileState(File); - FileState.State.FileState = EFileState::Unset; - FileState.State.TreeState = ETreeState::Unset; - FileState.State.LockState = ELockState::Unset; - // Search the file in the list of status - bool bFound = Results.RemoveAndCopyValue(File, Result); - if (bFound) + // Iterate on all files explicitly listed in the command + for (const auto& File : InFiles) { - // File found in status results; only the case for "changed" files - FGitStatusParser StatusParser(Result); + FGitSourceControlState FileState(File); + FileState.State.FileState = EFileState::Unset; + FileState.State.TreeState = ETreeState::Unset; + FileState.State.LockState = ELockState::Unset; + // Search the file in the list of status + bool bFound = Results.RemoveAndCopyValue(File, Result); + if (bFound) + { + // File found in status results; only the case for "changed" files + FGitStatusParser StatusParser(Result); #if UE_BUILD_DEBUG && GIT_DEBUG_STATUS - UE_LOG(LogSourceControl, Log, TEXT("Status(%s) = '%s' => File:%d, Tree:%d"), *File, *Result, static_cast(StatusParser.FileState), static_cast(StatusParser.TreeState)); + UE_LOG(LogSourceControl, Log, TEXT("Status(%s) = '%s' => File:%d, Tree:%d"), *File, *Result, + static_cast(StatusParser.FileState), static_cast(StatusParser.TreeState)); #endif - FileState.State.FileState = StatusParser.FileState; - FileState.State.TreeState = StatusParser.TreeState; - if (FileState.IsConflicted()) + FileState.State.FileState = StatusParser.FileState; + FileState.State.TreeState = StatusParser.TreeState; + if (FileState.IsConflicted()) + { + // In case of a conflict (unmerged file) get the base revision to merge + RunGetConflictStatus(InPathToGitBinary, InRepositoryRoot, File, FileState); + } + } + else { - // In case of a conflict (unmerged file) get the base revision to merge - RunGetConflictStatus(InPathToGitBinary, InRepositoryRoot, File, FileState); + FileState.State.FileState = EFileState::Unknown; + // File not found in status + if (FPaths::FileExists(File)) + { + // usually means the file is unchanged, + FileState.State.TreeState = ETreeState::Unmodified; +#if UE_BUILD_DEBUG && GIT_DEBUG_STATUS + UE_LOG(LogSourceControl, Log, TEXT("Status(%s) not found but exists => unchanged"), *File); +#endif + } + else + { + // but also the case for newly created content: there is no file on disk until the content is saved + // for the first time + FileState.State.TreeState = ETreeState::NotInRepo; +#if UE_BUILD_DEBUG && GIT_DEBUG_STATUS + UE_LOG(LogSourceControl, Log, TEXT("Status(%s) not found and does not exists => new/not controled"), + *File); +#endif + } } + if (!InUsingLfsLocking) + { + FileState.State.LockState = ELockState::Unlockable; + } + else + { + if (IsFileLFSLockable(File)) + { + if (!bCheckedLockedFiles) + { + bCheckedLockedFiles = true; + TArray ErrorMessages; + GetAllLocks(InRepositoryRoot, InPathToGitBinary, ErrorMessages, LockedFiles); + FTSMessageLog SourceControlLog("SourceControl"); + for (int32 ErrorIndex = 0; ErrorIndex < ErrorMessages.Num(); ++ErrorIndex) + { + SourceControlLog.Error(FText::FromString(ErrorMessages[ErrorIndex])); + } + } + if (LockedFiles.Contains(File)) + { + FileState.State.LockUser = LockedFiles[File]; + if (LfsUserName == FileState.State.LockUser) + { + FileState.State.LockState = ELockState::Locked; + } + else + { + FileState.State.LockState = ELockState::LockedOther; + } + } + else + { + FileState.State.LockState = ELockState::NotLocked; +#if UE_BUILD_DEBUG && GIT_DEBUG_STATUS + UE_LOG(LogSourceControl, Log, TEXT("Status(%s) Not Locked"), *File); +#endif + } + } + else + { + FileState.State.LockState = ELockState::Unlockable; + } + +#if UE_BUILD_DEBUG && GIT_DEBUG_STATUS + UE_LOG(LogSourceControl, Log, TEXT("Status(%s) Locked by '%s'"), *File, *FileState.State.LockUser); +#endif + } + OutStates.Add(File, MoveTemp(FileState)); } - else + + // The above cannot detect deleted assets since there is no file left to enumerate (either by the Content + // Browser or by git ls-files) + // => so we also parse the status results to explicitly look for Deleted/Missing assets + ParseDirectoryStatusResult(InUsingLfsLocking, Results, OutStates); + } + + void ParseStatusResults(const FString& InPathToGitBinary, const FString& InRepositoryRoot, + const bool InUsingLfsLocking, const TArray& InFiles, + const TMap& InResults, TMap& OutStates) + { + TSet Files; + for (const auto& File : InFiles) { - FileState.State.FileState = EFileState::Unknown; - // File not found in status - if (FPaths::FileExists(File)) + if (FPaths::DirectoryExists(File)) + { + TArray DirectoryFiles; + const bool bResult = + ListFilesInDirectoryRecurse(InPathToGitBinary, InRepositoryRoot, File, DirectoryFiles); + if (bResult) + { + for (const auto& InnerFile : DirectoryFiles) + { + Files.Add(InnerFile); + } + } + } + else + { + Files.Add(File); + } + } + ParseFileStatusResult(InPathToGitBinary, InRepositoryRoot, InUsingLfsLocking, Files, InResults, OutStates); + } + + void CheckRemote(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& Files, + TArray& OutErrorMessages, TMap& OutStates) + { + // We can obtain a list of files that were modified between our remote branches and HEAD. Assumes that fetch has + // been run to get accurate info. + + // Gather valid remote branches + FGitSourceControlModule* GitSourceControl = FGitSourceControlModule::GetThreadSafe(); + if (!GitSourceControl) + { + return; + } + FGitSourceControlProvider& Provider = GitSourceControl->GetProvider(); + const TArray StatusBranches = Provider.GetStatusBranchNames(); + + TSet BranchesToDiff {StatusBranches}; + + bool bDiffAgainstRemoteCurrent = false; + + // Get the current branch's remote. + FString CurrentBranchName; + if (GetRemoteBranchName(InPathToGitBinary, InRepositoryRoot, CurrentBranchName)) + { + // We have a valid remote, so diff against it. + bDiffAgainstRemoteCurrent = true; + // Ensure that the remote branch is in there. + BranchesToDiff.Add(CurrentBranchName); + } + + if (!BranchesToDiff.Num()) + { + return; + } + + TArray ErrorMessages; + + TArray Results; + TMap NewerFiles; + + // const TArray& RelativeFiles = RelativeFilenames(Files, InRepositoryRoot); + // Get the full remote status of the Content folder, since it's the only lockable folder we track in editor. + // This shows any new files as well. + // Also update the status of `.checksum`. + TArray FilesToDiff {FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()), ".checksum", + "Binaries/", "Plugins/"}; + TArray ParametersLog {TEXT("--pretty="), TEXT("--name-only"), TEXT(""), TEXT("--")}; + for (auto& Branch : BranchesToDiff) + { + bool bCurrentBranch; + if (bDiffAgainstRemoteCurrent && Branch.Equals(CurrentBranchName)) { - // usually means the file is unchanged, - FileState.State.TreeState = ETreeState::Unmodified; -#if UE_BUILD_DEBUG && GIT_DEBUG_STATUS - UE_LOG(LogSourceControl, Log, TEXT("Status(%s) not found but exists => unchanged"), *File); -#endif + bCurrentBranch = true; } else { - // but also the case for newly created content: there is no file on disk until the content is saved for the first time - FileState.State.TreeState = ETreeState::NotInRepo; -#if UE_BUILD_DEBUG && GIT_DEBUG_STATUS - UE_LOG(LogSourceControl, Log, TEXT("Status(%s) not found and does not exists => new/not controled"), *File); -#endif + bCurrentBranch = false; } - } - if (!InUsingLfsLocking) - { - FileState.State.LockState = ELockState::Unlockable; - } - else - { - if (IsFileLFSLockable(File)) + // empty defaults to HEAD + // .. means commits in the right that are not in the left + ParametersLog[2] = FString::Printf(TEXT("..%s"), *Branch); + + const bool bResultDiff = RunCommand(TEXT("log"), InPathToGitBinary, InRepositoryRoot, ParametersLog, + FilesToDiff, Results, ErrorMessages); + if (bResultDiff) { - if (!bCheckedLockedFiles) - { - bCheckedLockedFiles = true; - TArray ErrorMessages; - GetAllLocks(InRepositoryRoot, InPathToGitBinary, ErrorMessages, LockedFiles); - FTSMessageLog SourceControlLog("SourceControl"); - for (int32 ErrorIndex = 0; ErrorIndex < ErrorMessages.Num(); ++ErrorIndex) - { - SourceControlLog.Error(FText::FromString(ErrorMessages[ErrorIndex])); - } - } - if (LockedFiles.Contains(File)) + for (const FString& NewerFileName : Results) { - FileState.State.LockUser = LockedFiles[File]; - if (LfsUserName == FileState.State.LockUser) + // Don't care about mergeable files (.collection, .ini, .uproject, etc) + if (!IsFileLFSLockable(NewerFileName)) { - FileState.State.LockState = ELockState::Locked; + // Check if there's newer binaries pending on this branch + if (bCurrentBranch && (NewerFileName == TEXT(".checksum") || + NewerFileName.StartsWith("Binaries/", ESearchCase::IgnoreCase) || + NewerFileName.StartsWith("Plugins/", ESearchCase::IgnoreCase))) + { + Provider.bPendingRestart = true; + } + continue; } - else + const FString& NewerFilePath = FPaths::ConvertRelativePathToFull(InRepositoryRoot, NewerFileName); + if (bCurrentBranch || !NewerFiles.Contains(NewerFilePath)) { - FileState.State.LockState = ELockState::LockedOther; + NewerFiles.Add(NewerFilePath, Branch); } } - else - { - FileState.State.LockState = ELockState::NotLocked; -#if UE_BUILD_DEBUG && GIT_DEBUG_STATUS - UE_LOG(LogSourceControl, Log, TEXT("Status(%s) Not Locked"), *File); -#endif - } } - else - { - FileState.State.LockState = ELockState::Unlockable; - } - - -#if UE_BUILD_DEBUG && GIT_DEBUG_STATUS - UE_LOG(LogSourceControl, Log, TEXT("Status(%s) Locked by '%s'"), *File, *FileState.State.LockUser); -#endif + Results.Reset(); } - OutStates.Add(File, MoveTemp(FileState)); - } - // The above cannot detect deleted assets since there is no file left to enumerate (either by the Content Browser or by git ls-files) - // => so we also parse the status results to explicitly look for Deleted/Missing assets - ParseDirectoryStatusResult(InUsingLfsLocking, Results, OutStates); -} - -void ParseStatusResults(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool InUsingLfsLocking, const TArray& InFiles, - const TMap& InResults, TMap& OutStates) -{ - TSet Files; - for (const auto& File : InFiles) - { - if (FPaths::DirectoryExists(File)) + for (const auto& NewFile : NewerFiles) { - TArray DirectoryFiles; - const bool bResult = ListFilesInDirectoryRecurse(InPathToGitBinary, InRepositoryRoot, File, DirectoryFiles); - if (bResult) + if (FGitSourceControlState* FileState = OutStates.Find(NewFile.Key)) { - for (const auto& InnerFile : DirectoryFiles) - { - Files.Add(InnerFile); - } + FileState->State.RemoteState = + NewFile.Value.Equals(CurrentBranchName) ? ERemoteState::NotAtHead : ERemoteState::NotLatest; + FileState->State.HeadBranch = NewFile.Value; } } - else - { - Files.Add(File); - } - } - ParseFileStatusResult(InPathToGitBinary, InRepositoryRoot, InUsingLfsLocking, Files, InResults, OutStates); -} - -void CheckRemote(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& Files, - TArray& OutErrorMessages, TMap& OutStates) -{ - // We can obtain a list of files that were modified between our remote branches and HEAD. Assumes that fetch has been run to get accurate info. - - // Gather valid remote branches - FGitSourceControlModule* GitSourceControl = FGitSourceControlModule::GetThreadSafe(); - if (!GitSourceControl) - { - return; - } - FGitSourceControlProvider& Provider = GitSourceControl->GetProvider(); - const TArray StatusBranches = Provider.GetStatusBranchNames(); - - TSet BranchesToDiff{ StatusBranches }; - - bool bDiffAgainstRemoteCurrent = false; - - // Get the current branch's remote. - FString CurrentBranchName; - if (GetRemoteBranchName(InPathToGitBinary, InRepositoryRoot, CurrentBranchName)) - { - // We have a valid remote, so diff against it. - bDiffAgainstRemoteCurrent = true; - // Ensure that the remote branch is in there. - BranchesToDiff.Add(CurrentBranchName); - } - if (!BranchesToDiff.Num()) - { - return; + OutErrorMessages.Append(ErrorMessages); } - TArray ErrorMessages; - - TArray Results; - TMap NewerFiles; + const FTimespan CacheLimit = FTimespan::FromSeconds(30); - //const TArray& RelativeFiles = RelativeFilenames(Files, InRepositoryRoot); - // Get the full remote status of the Content folder, since it's the only lockable folder we track in editor. - // This shows any new files as well. - // Also update the status of `.checksum`. - TArray FilesToDiff{FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()), ".checksum", "Binaries/", "Plugins/"}; - TArray ParametersLog{TEXT("--pretty="), TEXT("--name-only"), TEXT(""), TEXT("--")}; - for (auto& Branch : BranchesToDiff) + bool GetAllLocks(const FString& InRepositoryRoot, const FString& GitBinaryFallback, + TArray& OutErrorMessages, TMap& OutLocks, bool bInvalidateCache) { - bool bCurrentBranch; - if (bDiffAgainstRemoteCurrent && Branch.Equals(CurrentBranchName)) + // You may ask, why are we ignoring state cache, and instead maintaining our own lock cache? + // The answer is that state cache updating is another operation, and those that update status + // (and thus the state cache) are using GetAllLocks. However, querying remote locks are almost always + // irrelevant in most of those update status cases. So, we need to provide a fast way to provide + // an updated local lock state. We could do this through the relevant lfs lock command arguments, which + // as you will see below, we use only for offline cases, but the exec cost of doing this isn't worth it + // when we can easily maintain this cache here. So, we are really emulating an internal Git LFS locks cache + // call, which gets fed into the state cache, rather than reimplementing the state cache :) + const FDateTime CurrentTime = FDateTime::Now(); + bool bCacheExpired = bInvalidateCache; + if (!bInvalidateCache) { - bCurrentBranch = true; + const FTimespan CacheTimeElapsed = CurrentTime - FGitLockedFilesCache::LastUpdated; + bCacheExpired = CacheTimeElapsed > CacheLimit; } - else + bool bResult = false; + if (bCacheExpired) { - bCurrentBranch = false; - } - // empty defaults to HEAD - // .. means commits in the right that are not in the left - ParametersLog[2] = FString::Printf(TEXT("..%s"), *Branch); + // Our cache expired, or they asked us to expire cache. Query locks directly from the remote server. + TArray ErrorMessages; + TArray Results; + bResult = FGitSourceControlModule::Get().GetLockProvider()->GetLockedFiles( + InRepositoryRoot, + FGitFileLockOpParams {GitBinaryFallback, FGitSourceControlModule::GetEmptyStringArray(), true, + FGitSourceControlModule::GetEmptyStringArray()}, + Results, OutErrorMessages); + if (bResult) + { + for (const FString& Result : Results) + { + FGitLfsLocksParser LockFile(InRepositoryRoot, Result); +#if UE_BUILD_DEBUG && GIT_DEBUG_STATUS + UE_LOG(LogSourceControl, Log, TEXT("LockedFile(%s, %s)"), *LockFile.LocalFilename, + *LockFile.LockUser); +#endif + OutLocks.Add(MoveTemp(LockFile.LocalFilename), MoveTemp(LockFile.LockUser)); + } + FGitLockedFilesCache::LastUpdated = CurrentTime; + FGitLockedFilesCache::SetLockedFiles(OutLocks); + return bResult; + } + // We tried to invalidate the UE cache, but we failed for some reason. Try updating lock state from LFS + // cache. Get the last known state of remote locks + TArray Params; + Params.Add(TEXT("--cached")); - const bool bResultDiff = RunCommand(TEXT("log"), InPathToGitBinary, InRepositoryRoot, ParametersLog, FilesToDiff, Results, ErrorMessages); - if (bResultDiff) - { - for (const FString& NewerFileName : Results) + FGitSourceControlModule* GitSourceControl = FGitSourceControlModule::GetThreadSafe(); + if (!GitSourceControl) + { + bResult = false; + } + else { - // Don't care about mergeable files (.collection, .ini, .uproject, etc) - if (!IsFileLFSLockable(NewerFileName)) + FGitSourceControlProvider& Provider = GitSourceControl->GetProvider(); + const FString& LockUser = Provider.GetLockUser(); + + Results.Reset(); + bResult = FGitSourceControlModule::Get().GetLockProvider()->GetLockedFiles( + InRepositoryRoot, + FGitFileLockOpParams {GitBinaryFallback, Params, false, + FGitSourceControlModule::GetEmptyStringArray()}, + Results, OutErrorMessages); + for (const FString& Result : Results) { - // Check if there's newer binaries pending on this branch - if (bCurrentBranch && (NewerFileName == TEXT(".checksum") || NewerFileName.StartsWith("Binaries/", ESearchCase::IgnoreCase) || - NewerFileName.StartsWith("Plugins/", ESearchCase::IgnoreCase))) + FGitLfsLocksParser LockFile(InRepositoryRoot, Result); +#if UE_BUILD_DEBUG && GIT_DEBUG_STATUS + UE_LOG(LogSourceControl, Log, TEXT("LockedFile(%s, %s)"), *LockFile.LocalFilename, + *LockFile.LockUser); +#endif + // Only update remote locks + if (LockFile.LockUser != LockUser) { - Provider.bPendingRestart = true; + OutLocks.Add(MoveTemp(LockFile.LocalFilename), MoveTemp(LockFile.LockUser)); } - continue; } - const FString& NewerFilePath = FPaths::ConvertRelativePathToFull(InRepositoryRoot, NewerFileName); - if (bCurrentBranch || !NewerFiles.Contains(NewerFilePath)) + // Get the latest local state of our own locks + Params.Reset(1); + Params.Add(TEXT("--local")); + + Results.Reset(); + bResult = FGitSourceControlModule::Get().GetLockProvider()->GetLockedFiles( + InRepositoryRoot, + FGitFileLockOpParams {GitBinaryFallback, Params, false, + FGitSourceControlModule::GetEmptyStringArray()}, + Results, OutErrorMessages); + for (const FString& Result : Results) { - NewerFiles.Add(NewerFilePath, Branch); + FGitLfsLocksParser LockFile(InRepositoryRoot, Result); +#if UE_BUILD_DEBUG && GIT_DEBUG_STATUS + UE_LOG(LogSourceControl, Log, TEXT("LockedFile(%s, %s)"), *LockFile.LocalFilename, + *LockFile.LockUser); +#endif + // Only update local locks + if (LockFile.LockUser == LockUser) + { + OutLocks.Add(MoveTemp(LockFile.LocalFilename), MoveTemp(LockFile.LockUser)); + } } } } - Results.Reset(); + if (!bResult) + { + // We can use our internally tracked local lock cache (an effective combination of --cached and --local) + OutLocks = FGitLockedFilesCache::GetLockedFiles(); + bResult = true; + } + return bResult; } - for (const auto& NewFile : NewerFiles) + void GetLockedFiles(const TArray& InFiles, TArray& OutFiles) { - if (FGitSourceControlState* FileState = OutStates.Find(NewFile.Key)) + FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); + FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); + + TArray> LocalStates; + Provider.GetState(InFiles, LocalStates, EStateCacheUsage::Use); + for (const auto& State : LocalStates) { - FileState->State.RemoteState = NewFile.Value.Equals(CurrentBranchName) ? ERemoteState::NotAtHead : ERemoteState::NotLatest; - FileState->State.HeadBranch = NewFile.Value; + const auto& GitState = StaticCastSharedRef(State); + if (GitState->State.LockState == ELockState::Locked) + { + OutFiles.Add(GitState->GetFilename()); + } } } - OutErrorMessages.Append(ErrorMessages); -} - -const FTimespan CacheLimit = FTimespan::FromSeconds(30); - -bool GetAllLocks(const FString& InRepositoryRoot, const FString& GitBinaryFallback, TArray& OutErrorMessages, TMap& OutLocks, bool bInvalidateCache) -{ - // You may ask, why are we ignoring state cache, and instead maintaining our own lock cache? - // The answer is that state cache updating is another operation, and those that update status - // (and thus the state cache) are using GetAllLocks. However, querying remote locks are almost always - // irrelevant in most of those update status cases. So, we need to provide a fast way to provide - // an updated local lock state. We could do this through the relevant lfs lock command arguments, which - // as you will see below, we use only for offline cases, but the exec cost of doing this isn't worth it - // when we can easily maintain this cache here. So, we are really emulating an internal Git LFS locks cache - // call, which gets fed into the state cache, rather than reimplementing the state cache :) - const FDateTime CurrentTime = FDateTime::Now(); - bool bCacheExpired = bInvalidateCache; - if (!bInvalidateCache) + FString GetFullPathFromGitStatus(const FString& Result, const FString& InRepositoryRoot) { - const FTimespan CacheTimeElapsed = CurrentTime - FGitLockedFilesCache::LastUpdated; - bCacheExpired = CacheTimeElapsed > CacheLimit; + const FString& RelativeFilename = FilenameFromGitStatus(Result); + FString File = FPaths::ConvertRelativePathToFull(InRepositoryRoot, RelativeFilename); + return File; } - bool bResult = false; - if (bCacheExpired) + + bool UpdateChangelistStateByCommand() { - // Our cache expired, or they asked us to expire cache. Query locks directly from the remote server. - TArray ErrorMessages; - TArray Results; - bResult = RunLFSCommand(TEXT("locks"), InRepositoryRoot, GitBinaryFallback, FGitSourceControlModule::GetEmptyStringArray(), FGitSourceControlModule::GetEmptyStringArray(), - Results, OutErrorMessages); - if (bResult) + // TODO: This is a temporary solution. + FModuleManager& ModuleManager = FModuleManager::Get(); + FName GitModuleName = "GitSourceControl"; + + if (!ModuleManager.IsModuleLoaded(GitModuleName)) { - for (const FString& Result : Results) - { - FGitLfsLocksParser LockFile(InRepositoryRoot, Result); -#if UE_BUILD_DEBUG && GIT_DEBUG_STATUS - UE_LOG(LogSourceControl, Log, TEXT("LockedFile(%s, %s)"), *LockFile.LocalFilename, *LockFile.LockUser); -#endif - OutLocks.Add(MoveTemp(LockFile.LocalFilename), MoveTemp(LockFile.LockUser)); - } - FGitLockedFilesCache::LastUpdated = CurrentTime; - FGitLockedFilesCache::SetLockedFiles(OutLocks); - return bResult; + UE_LOG(LogSourceControl, Warning, TEXT("GitSourceControl module is not loaded.")); + return false; } - // We tried to invalidate the UE cache, but we failed for some reason. Try updating lock state from LFS cache. - // Get the last known state of remote locks - TArray Params; - Params.Add(TEXT("--cached")); - FGitSourceControlModule* GitSourceControl = FGitSourceControlModule::GetThreadSafe(); - if (!GitSourceControl) + FGitSourceControlModule& GitSourceControl = + FModuleManager::GetModuleChecked("GitSourceControl"); + FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); + if (!Provider.IsGitAvailable()) { - bResult = false; + return false; } - else - { - FGitSourceControlProvider& Provider = GitSourceControl->GetProvider(); - const FString& LockUser = Provider.GetLockUser(); + TSharedRef StagedChangelist = + Provider.GetStateInternal(FGitSourceControlChangelist::StagedChangelist); + TSharedRef WorkingChangelist = + Provider.GetStateInternal(FGitSourceControlChangelist::WorkingChangelist); + StagedChangelist->Files.RemoveAll([](const FSourceControlStateRef& InState) { return true; }); + WorkingChangelist->Files.RemoveAll([](const FSourceControlStateRef& InState) { return true; }); - Results.Reset(); - bResult = RunLFSCommand(TEXT("locks"), InRepositoryRoot, GitBinaryFallback, Params, FGitSourceControlModule::GetEmptyStringArray(), Results, OutErrorMessages); - for (const FString& Result : Results) - { - FGitLfsLocksParser LockFile(InRepositoryRoot, Result); - #if UE_BUILD_DEBUG && GIT_DEBUG_STATUS - UE_LOG(LogSourceControl, Log, TEXT("LockedFile(%s, %s)"), *LockFile.LocalFilename, *LockFile.LockUser); - #endif - // Only update remote locks - if (LockFile.LockUser != LockUser) - { - OutLocks.Add(MoveTemp(LockFile.LocalFilename), MoveTemp(LockFile.LockUser)); - } + TArray Files; + Files.Add(TEXT("Content/")); + TArray Parameters; + Parameters.Add(TEXT("--porcelain")); + TArray Results; + TArray ErrorMsg; + const bool bResult = RunCommand(TEXT("--no-optional-locks status"), Provider.GetGitBinaryPath(), + Provider.GetPathToRepositoryRoot(), Parameters, Files, Results, ErrorMsg); + for (const auto& Result : Results) + { + FString File = GetFullPathFromGitStatus(Result, Provider.GetPathToRepositoryRoot()); + TSharedRef State = Provider.GetStateInternal(File); + // Staged check + if (!TChar::IsWhitespace(Result[0])) + { + WorkingChangelist->Files.Remove(State); + UpdateFileStagingOnSavedInternal(Result); + State->Changelist = FGitSourceControlChangelist::StagedChangelist; + StagedChangelist->Files.AddUnique(State); + continue; } - // Get the latest local state of our own locks - Params.Reset(1); - Params.Add(TEXT("--local")); - - Results.Reset(); - bResult &= RunLFSCommand(TEXT("locks"), InRepositoryRoot, GitBinaryFallback, Params, FGitSourceControlModule::GetEmptyStringArray(), Results, OutErrorMessages); - for (const FString& Result : Results) - { - FGitLfsLocksParser LockFile(InRepositoryRoot, Result); - #if UE_BUILD_DEBUG && GIT_DEBUG_STATUS - UE_LOG(LogSourceControl, Log, TEXT("LockedFile(%s, %s)"), *LockFile.LocalFilename, *LockFile.LockUser); - #endif - // Only update local locks - if (LockFile.LockUser == LockUser) - { - OutLocks.Add(MoveTemp(LockFile.LocalFilename), MoveTemp(LockFile.LockUser)); - } + // Working check + if (!TChar::IsWhitespace(Result[1])) + { + StagedChangelist->Files.Remove(State); + State->Changelist = FGitSourceControlChangelist::WorkingChangelist; + WorkingChangelist->Files.AddUnique(State); } } + return true; } - if (!bResult) + + // Run a batch of Git "status" command to update status of given files and/or directories. + bool RunUpdateStatus(const FString& InPathToGitBinary, const FString& InRepositoryRoot, + const bool InUsingLfsLocking, const TArray& InFiles, + TArray& OutErrorMessages, TMap& OutStates) { - // We can use our internally tracked local lock cache (an effective combination of --cached and --local) - OutLocks = FGitLockedFilesCache::GetLockedFiles(); - bResult = true; - } - return bResult; -} + // Remove files that aren't in the repository + const TArray& RepoFiles = InFiles.FilterByPredicate( + [InRepositoryRoot](const FString& File) { return File.StartsWith(InRepositoryRoot); }); -void GetLockedFiles(const TArray& InFiles, TArray& OutFiles) -{ - FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); - FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); + if (!RepoFiles.Num()) + { + return false; + } - TArray> LocalStates; - Provider.GetState(InFiles, LocalStates, EStateCacheUsage::Use); - for (const auto& State : LocalStates) - { - const auto& GitState = StaticCastSharedRef(State); - if (GitState->State.LockState == ELockState::Locked) + TArray Parameters; + Parameters.Add(TEXT("--porcelain")); + Parameters.Add(TEXT("-uall")); // make sure we use -uall to list all files instead of directories + // We skip checking ignored since no one ignores files that Unreal would read in as revision controlled + // (Content/{*.uasset,*.umap},Config/*.ini). + TArray Results; + // avoid locking the index when not needed (useful for status updates) + const bool bResult = RunCommand(TEXT("--no-optional-locks status"), InPathToGitBinary, InRepositoryRoot, + Parameters, RepoFiles, Results, OutErrorMessages); + TMap ResultsMap; + for (const auto& Result : Results) { - OutFiles.Add(GitState->GetFilename()); + const FString& RelativeFilename = FilenameFromGitStatus(Result); + const FString& File = FPaths::ConvertRelativePathToFull(InRepositoryRoot, RelativeFilename); + ResultsMap.Add(File, Result); + } + if (bResult) + { + ParseStatusResults(InPathToGitBinary, InRepositoryRoot, InUsingLfsLocking, RepoFiles, ResultsMap, + OutStates); } - } -} -FString GetFullPathFromGitStatus(const FString& Result, const FString& InRepositoryRoot) -{ - const FString& RelativeFilename = FilenameFromGitStatus(Result); - FString File = FPaths::ConvertRelativePathToFull(InRepositoryRoot, RelativeFilename); - return File; -} + UpdateChangelistStateByCommand(); -bool UpdateChangelistStateByCommand() -{ - // TODO: This is a temporary solution. - FModuleManager &ModuleManager = FModuleManager::Get(); - FName GitModuleName = "GitSourceControl"; + CheckRemote(InPathToGitBinary, InRepositoryRoot, RepoFiles, OutErrorMessages, OutStates); - if (!ModuleManager.IsModuleLoaded(GitModuleName)) - { - UE_LOG(LogSourceControl, Warning, TEXT("GitSourceControl module is not loaded.")); - return false; + return bResult; } - - FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); - FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); - if (!Provider.IsGitAvailable()) + + void UpdateFileStagingOnSaved(const FString& Filename, UPackage* Pkg, FObjectPostSaveContext ObjectSaveContext) { - return false; + UpdateFileStagingOnSavedInternal(Filename); } - TSharedRef StagedChangelist = Provider.GetStateInternal(FGitSourceControlChangelist::StagedChangelist); - TSharedRef WorkingChangelist = Provider.GetStateInternal(FGitSourceControlChangelist::WorkingChangelist); - StagedChangelist->Files.RemoveAll([](const FSourceControlStateRef& InState){ return true; }); - WorkingChangelist->Files.RemoveAll([](const FSourceControlStateRef& InState){ return true; }); - - TArray Files; - Files.Add(TEXT("Content/")); - TArray Parameters; - Parameters.Add(TEXT("--porcelain")); - TArray Results; - TArray ErrorMsg; - const bool bResult = RunCommand(TEXT("--no-optional-locks status"), Provider.GetGitBinaryPath(), Provider.GetPathToRepositoryRoot(), Parameters, Files, Results, ErrorMsg); - for (const auto& Result : Results) + + bool UpdateFileStagingOnSavedInternal(const FString& Filename) { - FString File = GetFullPathFromGitStatus(Result, Provider.GetPathToRepositoryRoot()); - TSharedRef State = Provider.GetStateInternal(File); - // Staged check - if (!TChar::IsWhitespace(Result[0])) + bool bResult = false; + FGitSourceControlModule& GitSourceControl = + FModuleManager::GetModuleChecked("GitSourceControl"); + FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); + if (!Provider.IsGitAvailable()) { - WorkingChangelist->Files.Remove(State); - UpdateFileStagingOnSavedInternal(Result); - State->Changelist = FGitSourceControlChangelist::StagedChangelist; - StagedChangelist->Files.AddUnique(State); - continue; + return bResult; } - // Working check - if (!TChar::IsWhitespace(Result[1])) + TSharedRef State = Provider.GetStateInternal(Filename); + + if (State->Changelist.GetName().Equals(TEXT("Staged"))) { - StagedChangelist->Files.Remove(State); - State->Changelist = FGitSourceControlChangelist::WorkingChangelist; - WorkingChangelist->Files.AddUnique(State); + TArray File; + File.Add(Filename); + TArray DummyResults; + TArray DummyMsgs; + bResult = RunCommand(TEXT("add"), Provider.GetGitBinaryPath(), Provider.GetPathToRepositoryRoot(), + FGitSourceControlModule::GetEmptyStringArray(), File, DummyResults, DummyMsgs); } - } - return true; -} - -// Run a batch of Git "status" command to update status of given files and/or directories. -bool RunUpdateStatus(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool InUsingLfsLocking, const TArray& InFiles, - TArray& OutErrorMessages, TMap& OutStates) -{ - // Remove files that aren't in the repository - const TArray& RepoFiles = InFiles.FilterByPredicate([InRepositoryRoot](const FString& File) { return File.StartsWith(InRepositoryRoot); }); - if (!RepoFiles.Num()) - { - return false; + return bResult; } - TArray Parameters; - Parameters.Add(TEXT("--porcelain")); - Parameters.Add(TEXT("-uall")); // make sure we use -uall to list all files instead of directories - // We skip checking ignored since no one ignores files that Unreal would read in as revision controlled (Content/{*.uasset,*.umap},Config/*.ini). - TArray Results; - // avoid locking the index when not needed (useful for status updates) - const bool bResult = RunCommand(TEXT("--no-optional-locks status"), InPathToGitBinary, InRepositoryRoot, Parameters, RepoFiles, Results, OutErrorMessages); - TMap ResultsMap; - for (const auto& Result : Results) + void UpdateStateOnAssetRename(const FAssetData& InAssetData, const FString& InOldName) { - const FString& RelativeFilename = FilenameFromGitStatus(Result); - const FString& File = FPaths::ConvertRelativePathToFull(InRepositoryRoot, RelativeFilename); - ResultsMap.Add(File, Result); + FGitSourceControlModule& GitSourceControl = + FModuleManager::GetModuleChecked("GitSourceControl"); + FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); + if (!Provider.IsGitAvailable()) + { + return; + } + TSharedRef State = Provider.GetStateInternal(InOldName); + + State->LocalFilename = InAssetData.GetObjectPathString(); } - if (bResult) + + // Run a Git `cat-file --filters` command to dump the binary content of a revision into a file. + bool RunDumpToFile(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InParameter, + const FString& InDumpFileName) { - ParseStatusResults(InPathToGitBinary, InRepositoryRoot, InUsingLfsLocking, RepoFiles, ResultsMap, OutStates); - } - - UpdateChangelistStateByCommand(); + int32 ReturnCode = -1; + FString FullCommand; - CheckRemote(InPathToGitBinary, InRepositoryRoot, RepoFiles, OutErrorMessages, OutStates); + FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); - return bResult; -} + if (!InRepositoryRoot.IsEmpty()) + { + // Specify the working copy (the root) of the git repository (before the command itself) + FullCommand = TEXT("-C \""); + FullCommand += InRepositoryRoot; + FullCommand += TEXT("\" "); + } -void UpdateFileStagingOnSaved(const FString& Filename, UPackage* Pkg, FObjectPostSaveContext ObjectSaveContext) -{ - UpdateFileStagingOnSavedInternal(Filename); -} - -bool UpdateFileStagingOnSavedInternal(const FString& Filename) -{ - bool bResult = false; - FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); - FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); - if (!Provider.IsGitAvailable()) - { - return bResult; - } - TSharedRef State = Provider.GetStateInternal(Filename); + // then the git command itself + // Newer versions (2.9.3.windows.2) support smudge/clean filters used by Git LFS, git-fat, git-annex, etc + FullCommand += TEXT("cat-file --filters "); - if (State->Changelist.GetName().Equals(TEXT("Staged"))) - { - TArray File; - File.Add(Filename); - TArray DummyResults; - TArray DummyMsgs; - bResult = RunCommand(TEXT("add"), Provider.GetGitBinaryPath(), Provider.GetPathToRepositoryRoot(), FGitSourceControlModule::GetEmptyStringArray(), File, DummyResults, DummyMsgs); - } - - return bResult; -} - -void UpdateStateOnAssetRename(const FAssetData& InAssetData, const FString& InOldName) -{ - FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); - FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); - if (!Provider.IsGitAvailable()) - { - return ; - } - TSharedRef State = Provider.GetStateInternal(InOldName); - - State->LocalFilename = InAssetData.GetObjectPathString(); -} + // Append to the command the parameter + FullCommand += TEXT("\"") + InParameter + TEXT("\""); -// Run a Git `cat-file --filters` command to dump the binary content of a revision into a file. -bool RunDumpToFile(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InParameter, const FString& InDumpFileName) -{ - int32 ReturnCode = -1; - FString FullCommand; + const bool bLaunchDetached = false; + const bool bLaunchHidden = true; + const bool bLaunchReallyHidden = bLaunchHidden; - FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); + void* PipeRead = nullptr; + void* PipeWrite = nullptr; - if (!InRepositoryRoot.IsEmpty()) - { - // Specify the working copy (the root) of the git repository (before the command itself) - FullCommand = TEXT("-C \""); - FullCommand += InRepositoryRoot; - FullCommand += TEXT("\" "); - } + verify(FPlatformProcess::CreatePipe(PipeRead, PipeWrite)); - // then the git command itself - // Newer versions (2.9.3.windows.2) support smudge/clean filters used by Git LFS, git-fat, git-annex, etc - FullCommand += TEXT("cat-file --filters "); - - // Append to the command the parameter - FullCommand += TEXT("\"") + InParameter + TEXT("\""); - - const bool bLaunchDetached = false; - const bool bLaunchHidden = true; - const bool bLaunchReallyHidden = bLaunchHidden; - - void* PipeRead = nullptr; - void* PipeWrite = nullptr; - - verify(FPlatformProcess::CreatePipe(PipeRead, PipeWrite)); - - UE_LOG(LogSourceControl, Log, TEXT("RunDumpToFile: 'git %s'"), *FullCommand); - - FString PathToGitOrEnvBinary = InPathToGitBinary; - #if PLATFORM_MAC - // The Cocoa application does not inherit shell environment variables, so add the path expected to have git-lfs to PATH - FString PathEnv = FPlatformMisc::GetEnvironmentVariable(TEXT("PATH")); - FString GitInstallPath = FPaths::GetPath(InPathToGitBinary); - - TArray PathArray; - PathEnv.ParseIntoArray(PathArray, FPlatformMisc::GetPathVarDelimiter()); - bool bHasGitInstallPath = false; - for (auto Path : PathArray) - { - if (GitInstallPath.Equals(Path, ESearchCase::CaseSensitive)) - { - bHasGitInstallPath = true; - break; - } - } - - if (!bHasGitInstallPath) - { - PathToGitOrEnvBinary = FString("/usr/bin/env"); - FullCommand = FString::Printf(TEXT("PATH=\"%s%s%s\" \"%s\" %s"), *GitInstallPath, FPlatformMisc::GetPathVarDelimiter(), *PathEnv, *InPathToGitBinary, *FullCommand); - } - #endif + UE_LOG(LogSourceControl, Log, TEXT("RunDumpToFile: 'git %s'"), *FullCommand); + + FString PathToGitOrEnvBinary = InPathToGitBinary; +#if PLATFORM_MAC + // The Cocoa application does not inherit shell environment variables, so add the path expected to have git-lfs + // to PATH + FString PathEnv = FPlatformMisc::GetEnvironmentVariable(TEXT("PATH")); + FString GitInstallPath = FPaths::GetPath(InPathToGitBinary); + + TArray PathArray; + PathEnv.ParseIntoArray(PathArray, FPlatformMisc::GetPathVarDelimiter()); + bool bHasGitInstallPath = false; + for (auto Path : PathArray) + { + if (GitInstallPath.Equals(Path, ESearchCase::CaseSensitive)) + { + bHasGitInstallPath = true; + break; + } + } + + if (!bHasGitInstallPath) + { + PathToGitOrEnvBinary = FString("/usr/bin/env"); + FullCommand = + FString::Printf(TEXT("PATH=\"%s%s%s\" \"%s\" %s"), *GitInstallPath, + FPlatformMisc::GetPathVarDelimiter(), *PathEnv, *InPathToGitBinary, *FullCommand); + } +#endif #if ENGINE_MAJOR_VERSION == 5 && 0 - FProcHandle ProcessHandle = FPlatformProcess::CreateProc(*PathToGitOrEnvBinary, *FullCommand, bLaunchDetached, bLaunchHidden, bLaunchReallyHidden, nullptr, 0, *InRepositoryRoot, PipeWrite, nullptr, nullptr); + FProcHandle ProcessHandle = FPlatformProcess::CreateProc(*PathToGitOrEnvBinary, *FullCommand, bLaunchDetached, + bLaunchHidden, bLaunchReallyHidden, nullptr, 0, + *InRepositoryRoot, PipeWrite, nullptr, nullptr); #else - FProcHandle ProcessHandle = FPlatformProcess::CreateProc(*PathToGitOrEnvBinary, *FullCommand, bLaunchDetached, bLaunchHidden, bLaunchReallyHidden, nullptr, 0, *InRepositoryRoot, PipeWrite); + FProcHandle ProcessHandle = + FPlatformProcess::CreateProc(*PathToGitOrEnvBinary, *FullCommand, bLaunchDetached, bLaunchHidden, + bLaunchReallyHidden, nullptr, 0, *InRepositoryRoot, PipeWrite); #endif - if(ProcessHandle.IsValid()) - { - FPlatformProcess::Sleep(0.01f); - - TArray BinaryFileContent; - bool bRemovedLFSMessage = false; - while (FPlatformProcess::IsProcRunning(ProcessHandle)) + if (ProcessHandle.IsValid()) { + FPlatformProcess::Sleep(0.01f); + + TArray BinaryFileContent; + bool bRemovedLFSMessage = false; + while (FPlatformProcess::IsProcRunning(ProcessHandle)) + { + TArray BinaryData; + FPlatformProcess::ReadPipeToArray(PipeRead, BinaryData); + if (BinaryData.Num() > 0) + { + if (GitSourceControl.AccessSettings().IsUsingGitLfsLocking()) + { + // @todo: this is hacky! + if (BinaryData[0] == 68) // Check for D in "Downloading" + { + if (BinaryData[BinaryData.Num() - 1] == 10) // Check for newline + { + BinaryData.Reset(); + bRemovedLFSMessage = true; + } + } + else + { + BinaryFileContent.Append(MoveTemp(BinaryData)); + } + } + else + { + BinaryFileContent.Append(MoveTemp(BinaryData)); + } + } + } TArray BinaryData; FPlatformProcess::ReadPipeToArray(PipeRead, BinaryData); if (BinaryData.Num() > 0) @@ -1841,12 +1953,20 @@ bool RunDumpToFile(const FString& InPathToGitBinary, const FString& InRepository if (GitSourceControl.AccessSettings().IsUsingGitLfsLocking()) { // @todo: this is hacky! - if (BinaryData[0] == 68) // Check for D in "Downloading" + if (!bRemovedLFSMessage && BinaryData[0] == 68) // Check for D in "Downloading" { - if (BinaryData[BinaryData.Num() - 1] == 10) // Check for newline + int32 NewLineIndex = 0; + for (int32 Index = 0; Index < BinaryData.Num(); Index++) + { + if (BinaryData[Index] == 10) // Check for newline + { + NewLineIndex = Index; + break; + } + } + if (NewLineIndex > 0) { - BinaryData.Reset(); - bRemovedLFSMessage = true; + BinaryData.RemoveAt(0, NewLineIndex + 1); } } else @@ -1859,677 +1979,674 @@ bool RunDumpToFile(const FString& InPathToGitBinary, const FString& InRepository BinaryFileContent.Append(MoveTemp(BinaryData)); } } - } - TArray BinaryData; - FPlatformProcess::ReadPipeToArray(PipeRead, BinaryData); - if (BinaryData.Num() > 0) - { - if (GitSourceControl.AccessSettings().IsUsingGitLfsLocking()) + + FPlatformProcess::GetProcReturnCode(ProcessHandle, &ReturnCode); + if (ReturnCode == 0) { - // @todo: this is hacky! - if (!bRemovedLFSMessage && BinaryData[0] == 68) // Check for D in "Downloading" + // Save buffer into temp file + if (FFileHelper::SaveArrayToFile(BinaryFileContent, *InDumpFileName)) { - int32 NewLineIndex = 0; - for (int32 Index = 0; Index < BinaryData.Num(); Index++) - { - if (BinaryData[Index] == 10) // Check for newline - { - NewLineIndex = Index; - break; - } - } - if (NewLineIndex > 0) - { - BinaryData.RemoveAt(0, NewLineIndex + 1); - } + UE_LOG(LogSourceControl, Log, TEXT("Wrote '%s' (%do)"), *InDumpFileName, BinaryFileContent.Num()); } else { - BinaryFileContent.Append(MoveTemp(BinaryData)); + UE_LOG(LogSourceControl, Error, TEXT("Could not write %s"), *InDumpFileName); + ReturnCode = -1; } } else { - BinaryFileContent.Append(MoveTemp(BinaryData)); + UE_LOG(LogSourceControl, Error, TEXT("DumpToFile: ReturnCode=%d"), ReturnCode); + } + + FPlatformProcess::CloseProc(ProcessHandle); + } + else + { + UE_LOG(LogSourceControl, Error, TEXT("Failed to launch 'git cat-file'")); + } + + FPlatformProcess::ClosePipe(PipeRead, PipeWrite); + + return (ReturnCode == 0); + } + + /** + * Translate file actions from the given Git log --name-status command to keywords used by the Editor UI. + * + * @see https://www.kernel.org/pub/software/scm/git/docs/git-log.html + * ' ' = unmodified + * 'M' = modified + * 'A' = added + * 'D' = deleted + * 'R' = renamed + * 'C' = copied + * 'T' = type changed + * 'U' = updated but unmerged + * 'X' = unknown + * 'B' = broken pairing + * + * @see SHistoryRevisionListRowContent::GenerateWidgetForColumn(): "add", "edit", "delete", "branch" and "integrate" + * (everything else is taken like "edit") + */ + static FString LogStatusToString(TCHAR InStatus) + { + switch (InStatus) + { + case TEXT(' '): + return FString("unmodified"); + case TEXT('M'): + return FString("modified"); + case TEXT('A'): // added: keyword "add" to display a specific icon instead of the default "edit" action one + return FString("add"); + case TEXT( + 'D'): // deleted: keyword "delete" to display a specific icon instead of the default "edit" action one + return FString("delete"); + case TEXT( + 'R'): // renamed keyword "branch" to display a specific icon instead of the default "edit" action one + return FString("branch"); + case TEXT( + 'C'): // copied keyword "branch" to display a specific icon instead of the default "edit" action one + return FString("branch"); + case TEXT('T'): + return FString("type changed"); + case TEXT('U'): + return FString("unmerged"); + case TEXT('X'): + return FString("unknown"); + case TEXT('B'): + return FString("broked pairing"); + } + + return FString(); + } + + /** + * Parse the array of strings results of a 'git log' command + * + * Example git log results: + commit 97a4e7626681895e073aaefd68b8ac087db81b0b + Author: Sébastien Rombauts + Date: 2014-2015-05-15 21:32:27 +0200 + + Another commit used to test History + + - with many lines + - some + - and strange characteres $*+ + + M Content/Blueprints/Blueprint_CeilingLight.uasset + R100 Content/Textures/T_Concrete_Poured_D.uasset Content/Textures/T_Concrete_Poured_D2.uasset + + commit 355f0df26ebd3888adbb558fd42bb8bd3e565000 + Author: Sébastien Rombauts + Date: 2014-2015-05-12 11:28:14 +0200 + + Testing git status, edit, and revert + + A Content/Blueprints/Blueprint_CeilingLight.uasset + C099 Content/Textures/T_Concrete_Poured_N.uasset Content/Textures/T_Concrete_Poured_N2.uasset + */ + static void ParseLogResults(const TArray& InResults, TGitSourceControlHistory& OutHistory) + { + TSharedRef SourceControlRevision = + MakeShareable(new FGitSourceControlRevision); + for (const auto& Result : InResults) + { + if (Result.StartsWith(TEXT("commit "))) // Start of a new commit + { + // End of the previous commit + if (SourceControlRevision->RevisionNumber != 0) + { + OutHistory.Add(MoveTemp(SourceControlRevision)); + + SourceControlRevision = MakeShareable(new FGitSourceControlRevision); + } + SourceControlRevision->CommitId = Result.RightChop(7); // Full commit SHA1 hexadecimal string + SourceControlRevision->ShortCommitId = + SourceControlRevision->CommitId.Left(8); // Short revision ; first 8 hex characters (max that can + // hold a 32 bit integer) + SourceControlRevision->CommitIdNumber = FParse::HexNumber(*SourceControlRevision->ShortCommitId); + SourceControlRevision->RevisionNumber = + -1; // RevisionNumber will be set at the end, based off the index in the History + } + else if (Result.StartsWith(TEXT("Author: "))) // Author name & email + { + // Remove the 'email' part of the UserName + FString UserNameEmail = Result.RightChop(8); + int32 EmailIndex = 0; + if (UserNameEmail.FindLastChar('<', EmailIndex)) + { + SourceControlRevision->UserName = UserNameEmail.Left(EmailIndex - 1); + } + } + else if (Result.StartsWith(TEXT("Date: "))) // Commit date + { + FString Date = Result.RightChop(8); + SourceControlRevision->Date = FDateTime::FromUnixTimestamp(FCString::Atoi(*Date)); + } + // else if(Result.IsEmpty()) // empty line before/after commit message has already been taken care by + // FString::ParseIntoArray() + else if (Result.StartsWith(TEXT(" "))) // Multi-lines commit message + { + SourceControlRevision->Description += Result.RightChop(4); + SourceControlRevision->Description += TEXT("\n"); + } + else // Name of the file, starting with an uppercase status letter ("A"/"M"...) + { + const TCHAR Status = Result[0]; + SourceControlRevision->Action = + LogStatusToString(Status); // Readable action string ("Added", Modified"...) instead of "A"/"M"... + // Take care of special case for Renamed/Copied file: extract the second filename after second + // tabulation + int32 IdxTab; + if (Result.FindLastChar('\t', IdxTab)) + { + SourceControlRevision->Filename = Result.RightChop(IdxTab + 1); // relative filename + } + } + } + // End of the last commit + if (SourceControlRevision->RevisionNumber != 0) + { + OutHistory.Add(MoveTemp(SourceControlRevision)); + } + + // Then set the revision number of each Revision based on its index (reverse order since the log starts with the + // most recent change) + for (int32 RevisionIndex = 0; RevisionIndex < OutHistory.Num(); RevisionIndex++) + { + const auto& SourceControlRevisionItem = OutHistory[RevisionIndex]; + SourceControlRevisionItem->RevisionNumber = OutHistory.Num() - RevisionIndex; + + // Special case of a move ("branch" in Perforce term): point to the previous change (so the next one in the + // order of the log) + if ((SourceControlRevisionItem->Action == "branch") && (RevisionIndex < OutHistory.Num() - 1)) + { + SourceControlRevisionItem->BranchSource = OutHistory[RevisionIndex + 1]; + } + } + } + + /** + * Extract the SHA1 identifier and size of a blob (file) from a Git "ls-tree" command. + * + * Example output for the command git ls-tree --long 7fdaeb2 Content/Blueprints/BP_Test.uasset + 100644 blob a14347dc3b589b78fb19ba62a7e3982f343718bc 70731 Content/Blueprints/BP_Test.uasset + */ + class FGitLsTreeParser + { + public: + /** Parse the unmerge status: extract the base SHA1 identifier of the file */ + FGitLsTreeParser(const TArray& InResults) + { + const FString& FirstResult = InResults[0]; + FileHash = FirstResult.Mid(12, 40); + int32 IdxTab; + if (FirstResult.FindChar('\t', IdxTab)) + { + const FString SizeString = FirstResult.Mid(53, IdxTab - 53); + FileSize = FCString::Atoi(*SizeString); } } - FPlatformProcess::GetProcReturnCode(ProcessHandle, &ReturnCode); - if (ReturnCode == 0) + FString FileHash; ///< SHA1 Id of the file (warning: not the commit Id) + int32 FileSize; ///< Size of the file (in bytes) + }; + + // Run a Git "log" command and parse it. + bool RunGetHistory(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InFile, + bool bMergeConflict, TArray& OutErrorMessages, TGitSourceControlHistory& OutHistory) + { + bool bResults; { - // Save buffer into temp file - if (FFileHelper::SaveArrayToFile(BinaryFileContent, *InDumpFileName)) + TArray Results; + TArray Parameters; + Parameters.Add(TEXT("--follow")); // follow file renames + Parameters.Add(TEXT("--date=raw")); + Parameters.Add(TEXT("--name-status")); // relative filename at this revision, preceded by a status character + Parameters.Add(TEXT("--pretty=medium")); // make sure format matches expected in ParseLogResults + if (bMergeConflict) { - UE_LOG(LogSourceControl, Log, TEXT("Wrote '%s' (%do)"), *InDumpFileName, BinaryFileContent.Num()); + // 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) + // @todo does not work for a cherry-pick! Test for a rebase. + Parameters.Add(TEXT("MERGE_HEAD")); + Parameters.Add(TEXT("--max-count 1")); } else { - UE_LOG(LogSourceControl, Error, TEXT("Could not write %s"), *InDumpFileName); - ReturnCode = -1; + Parameters.Add(TEXT("--max-count 250")); // Increase default count to 250 from 100 + } + TArray Files; + Files.Add(*InFile); + bResults = RunCommand(TEXT("log"), InPathToGitBinary, InRepositoryRoot, Parameters, Files, Results, + OutErrorMessages); + if (bResults) + { + ParseLogResults(Results, OutHistory); } } - else - { - UE_LOG(LogSourceControl, Error, TEXT("DumpToFile: ReturnCode=%d"), ReturnCode); + for (auto& Revision : OutHistory) + { + // Get file (blob) sha1 id and size + TArray Results; + TArray Parameters; + Parameters.Add(TEXT("--long")); // Show object size of blob (file) entries. + Parameters.Add(Revision->GetRevision()); + TArray Files; + Files.Add(*Revision->GetFilename()); + bResults &= RunCommand(TEXT("ls-tree"), InPathToGitBinary, InRepositoryRoot, Parameters, Files, Results, + OutErrorMessages); + if (bResults && Results.Num()) + { + FGitLsTreeParser LsTree(Results); + Revision->FileHash = LsTree.FileHash; + Revision->FileSize = LsTree.FileSize; + } + Revision->PathToRepoRoot = InRepositoryRoot; } - FPlatformProcess::CloseProc(ProcessHandle); - } - else - { - UE_LOG(LogSourceControl, Error, TEXT("Failed to launch 'git cat-file'")); - } - - FPlatformProcess::ClosePipe(PipeRead, PipeWrite); - - return (ReturnCode == 0); -} - -/** - * Translate file actions from the given Git log --name-status command to keywords used by the Editor UI. - * - * @see https://www.kernel.org/pub/software/scm/git/docs/git-log.html - * ' ' = unmodified - * 'M' = modified - * 'A' = added - * 'D' = deleted - * 'R' = renamed - * 'C' = copied - * 'T' = type changed - * 'U' = updated but unmerged - * 'X' = unknown - * 'B' = broken pairing - * - * @see SHistoryRevisionListRowContent::GenerateWidgetForColumn(): "add", "edit", "delete", "branch" and "integrate" (everything else is taken like "edit") - */ -static FString LogStatusToString(TCHAR InStatus) -{ - switch (InStatus) - { - case TEXT(' '): - return FString("unmodified"); - case TEXT('M'): - return FString("modified"); - case TEXT('A'): // added: keyword "add" to display a specific icon instead of the default "edit" action one - return FString("add"); - case TEXT('D'): // deleted: keyword "delete" to display a specific icon instead of the default "edit" action one - return FString("delete"); - case TEXT('R'): // renamed keyword "branch" to display a specific icon instead of the default "edit" action one - return FString("branch"); - case TEXT('C'): // copied keyword "branch" to display a specific icon instead of the default "edit" action one - return FString("branch"); - case TEXT('T'): - return FString("type changed"); - case TEXT('U'): - return FString("unmerged"); - case TEXT('X'): - return FString("unknown"); - case TEXT('B'): - return FString("broked pairing"); + return bResults; } - return FString(); -} - -/** - * Parse the array of strings results of a 'git log' command - * - * Example git log results: -commit 97a4e7626681895e073aaefd68b8ac087db81b0b -Author: Sébastien Rombauts -Date: 2014-2015-05-15 21:32:27 +0200 - - Another commit used to test History - - - with many lines - - some - - and strange characteres $*+ - -M Content/Blueprints/Blueprint_CeilingLight.uasset -R100 Content/Textures/T_Concrete_Poured_D.uasset Content/Textures/T_Concrete_Poured_D2.uasset - -commit 355f0df26ebd3888adbb558fd42bb8bd3e565000 -Author: Sébastien Rombauts -Date: 2014-2015-05-12 11:28:14 +0200 - - Testing git status, edit, and revert - -A Content/Blueprints/Blueprint_CeilingLight.uasset -C099 Content/Textures/T_Concrete_Poured_N.uasset Content/Textures/T_Concrete_Poured_N2.uasset -*/ -static void ParseLogResults(const TArray& InResults, TGitSourceControlHistory& OutHistory) -{ - TSharedRef SourceControlRevision = MakeShareable(new FGitSourceControlRevision); - for (const auto& Result : InResults) + TArray RelativeFilenames(const TArray& InFileNames, const FString& InRelativeTo) { - if (Result.StartsWith(TEXT("commit "))) // Start of a new commit - { - // End of the previous commit - if (SourceControlRevision->RevisionNumber != 0) - { - OutHistory.Add(MoveTemp(SourceControlRevision)); + TArray RelativeFiles; + FString RelativeTo = InRelativeTo; - SourceControlRevision = MakeShareable(new FGitSourceControlRevision); - } - SourceControlRevision->CommitId = Result.RightChop(7); // Full commit SHA1 hexadecimal string - SourceControlRevision->ShortCommitId = SourceControlRevision->CommitId.Left(8); // Short revision ; first 8 hex characters (max that can hold a 32 - // bit integer) - SourceControlRevision->CommitIdNumber = FParse::HexNumber(*SourceControlRevision->ShortCommitId); - SourceControlRevision->RevisionNumber = -1; // RevisionNumber will be set at the end, based off the index in the History - } - else if (Result.StartsWith(TEXT("Author: "))) // Author name & email + // Ensure that the path ends w/ '/' + if ((RelativeTo.Len() > 0) && (RelativeTo.EndsWith(TEXT("/"), ESearchCase::CaseSensitive) == false) && + (RelativeTo.EndsWith(TEXT("\\"), ESearchCase::CaseSensitive) == false)) { - // Remove the 'email' part of the UserName - FString UserNameEmail = Result.RightChop(8); - int32 EmailIndex = 0; - if (UserNameEmail.FindLastChar('<', EmailIndex)) - { - SourceControlRevision->UserName = UserNameEmail.Left(EmailIndex - 1); - } - } - else if (Result.StartsWith(TEXT("Date: "))) // Commit date - { - FString Date = Result.RightChop(8); - SourceControlRevision->Date = FDateTime::FromUnixTimestamp(FCString::Atoi(*Date)); + RelativeTo += TEXT("/"); } - // else if(Result.IsEmpty()) // empty line before/after commit message has already been taken care by FString::ParseIntoArray() - else if (Result.StartsWith(TEXT(" "))) // Multi-lines commit message + for (FString FileName : InFileNames) // string copy to be able to convert it inplace { - SourceControlRevision->Description += Result.RightChop(4); - SourceControlRevision->Description += TEXT("\n"); - } - else // Name of the file, starting with an uppercase status letter ("A"/"M"...) - { - const TCHAR Status = Result[0]; - SourceControlRevision->Action = LogStatusToString(Status); // Readable action string ("Added", Modified"...) instead of "A"/"M"... - // Take care of special case for Renamed/Copied file: extract the second filename after second tabulation - int32 IdxTab; - if (Result.FindLastChar('\t', IdxTab)) + if (FPaths::MakePathRelativeTo(FileName, *RelativeTo)) { - SourceControlRevision->Filename = Result.RightChop(IdxTab + 1); // relative filename + RelativeFiles.Add(FileName); } } - } - // End of the last commit - if (SourceControlRevision->RevisionNumber != 0) - { - OutHistory.Add(MoveTemp(SourceControlRevision)); + + return RelativeFiles; } - // Then set the revision number of each Revision based on its index (reverse order since the log starts with the most recent change) - for (int32 RevisionIndex = 0; RevisionIndex < OutHistory.Num(); RevisionIndex++) + TArray AbsoluteFilenames(const TArray& InFileNames, const FString& InRelativeTo) { - const auto& SourceControlRevisionItem = OutHistory[RevisionIndex]; - SourceControlRevisionItem->RevisionNumber = OutHistory.Num() - RevisionIndex; + TArray AbsFiles; - // Special case of a move ("branch" in Perforce term): point to the previous change (so the next one in the order of the log) - if ((SourceControlRevisionItem->Action == "branch") && (RevisionIndex < OutHistory.Num() - 1)) + for (FString FileName : InFileNames) // string copy to be able to convert it inplace { - SourceControlRevisionItem->BranchSource = OutHistory[RevisionIndex + 1]; + AbsFiles.Add(FPaths::Combine(InRelativeTo, FileName)); } - } -} -/** - * Extract the SHA1 identifier and size of a blob (file) from a Git "ls-tree" command. - * - * Example output for the command git ls-tree --long 7fdaeb2 Content/Blueprints/BP_Test.uasset -100644 blob a14347dc3b589b78fb19ba62a7e3982f343718bc 70731 Content/Blueprints/BP_Test.uasset -*/ -class FGitLsTreeParser -{ -public: - /** Parse the unmerge status: extract the base SHA1 identifier of the file */ - FGitLsTreeParser(const TArray& InResults) - { - const FString& FirstResult = InResults[0]; - FileHash = FirstResult.Mid(12, 40); - int32 IdxTab; - if (FirstResult.FindChar('\t', IdxTab)) - { - const FString SizeString = FirstResult.Mid(53, IdxTab - 53); - FileSize = FCString::Atoi(*SizeString); - } + return AbsFiles; } - FString FileHash; ///< SHA1 Id of the file (warning: not the commit Id) - int32 FileSize; ///< Size of the file (in bytes) -}; - -// Run a Git "log" command and parse it. -bool RunGetHistory(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InFile, bool bMergeConflict, - TArray& OutErrorMessages, TGitSourceControlHistory& OutHistory) -{ - bool bResults; + bool UpdateCachedStates(const TMap& InResults) { - TArray Results; - TArray Parameters; - Parameters.Add(TEXT("--follow")); // follow file renames - Parameters.Add(TEXT("--date=raw")); - Parameters.Add(TEXT("--name-status")); // relative filename at this revision, preceded by a status character - Parameters.Add(TEXT("--pretty=medium")); // make sure format matches expected in ParseLogResults - if (bMergeConflict) + if (InResults.Num() == 0) { - // 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) - // @todo does not work for a cherry-pick! Test for a rebase. - Parameters.Add(TEXT("MERGE_HEAD")); - Parameters.Add(TEXT("--max-count 1")); + return false; } - else + + FGitSourceControlModule* GitSourceControl = FGitSourceControlModule::GetThreadSafe(); + if (!GitSourceControl) { - Parameters.Add(TEXT("--max-count 250")); // Increase default count to 250 from 100 + return false; } - TArray Files; - Files.Add(*InFile); - bResults = RunCommand(TEXT("log"), InPathToGitBinary, InRepositoryRoot, Parameters, Files, Results, OutErrorMessages); - if (bResults) + 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(); + + for (const auto& Pair : InResults) { - ParseLogResults(Results, OutHistory); + TSharedRef State = Provider.GetStateInternal(Pair.Key); + const FGitState& NewState = Pair.Value; + if (NewState.FileState != EFileState::Unset) + { + // Invalid transition + if (NewState.FileState == EFileState::Added && !State->IsUnknown() && !State->CanAdd()) + { + continue; + } + State->State.FileState = NewState.FileState; + } + if (NewState.TreeState != ETreeState::Unset) + { + State->State.TreeState = NewState.TreeState; + } + // If we're updating lock state, also update user + if (NewState.LockState != ELockState::Unset) + { + State->State.LockState = NewState.LockState; + State->State.LockUser = NewState.LockUser; + } + if (NewState.RemoteState != ERemoteState::Unset) + { + State->State.RemoteState = NewState.RemoteState; + if (NewState.RemoteState == ERemoteState::UpToDate) + { + State->State.HeadBranch = TEXT(""); + } + else + { + State->State.HeadBranch = NewState.HeadBranch; + } + } + State->TimeStamp = Now; + + // We've just updated the state, no need for UpdateStatus to be ran for this file again. + Provider.AddFileToIgnoreForceCache(State->LocalFilename); } + + return true; } - for (auto& Revision : OutHistory) + + bool CollectNewStates(const TMap& InStates, + TMap& OutResults) { - // Get file (blob) sha1 id and size - TArray Results; - TArray Parameters; - Parameters.Add(TEXT("--long")); // Show object size of blob (file) entries. - Parameters.Add(Revision->GetRevision()); - TArray Files; - Files.Add(*Revision->GetFilename()); - bResults &= RunCommand(TEXT("ls-tree"), InPathToGitBinary, InRepositoryRoot, Parameters, Files, Results, OutErrorMessages); - if (bResults && Results.Num()) + if (InStates.Num() == 0) { - FGitLsTreeParser LsTree(Results); - Revision->FileHash = LsTree.FileHash; - Revision->FileSize = LsTree.FileSize; + return false; } - Revision->PathToRepoRoot = InRepositoryRoot; - } - - return bResults; -} - -TArray RelativeFilenames(const TArray& InFileNames, const FString& InRelativeTo) -{ - TArray RelativeFiles; - FString RelativeTo = InRelativeTo; - // Ensure that the path ends w/ '/' - if ((RelativeTo.Len() > 0) && (RelativeTo.EndsWith(TEXT("/"), ESearchCase::CaseSensitive) == false) && - (RelativeTo.EndsWith(TEXT("\\"), ESearchCase::CaseSensitive) == false)) - { - RelativeTo += TEXT("/"); - } - for (FString FileName : InFileNames) // string copy to be able to convert it inplace - { - if (FPaths::MakePathRelativeTo(FileName, *RelativeTo)) + for (const auto& InState : InStates) { - RelativeFiles.Add(FileName); + OutResults.Add(InState.Key, InState.Value.State); } - } - - return RelativeFiles; -} - -TArray AbsoluteFilenames(const TArray& InFileNames, const FString& InRelativeTo) -{ - TArray AbsFiles; - for(FString FileName : InFileNames) // string copy to be able to convert it inplace - { - AbsFiles.Add(FPaths::Combine(InRelativeTo, FileName)); + return true; } - return AbsFiles; -} - -bool UpdateCachedStates(const TMap& InResults) -{ - if (InResults.Num() == 0) + bool CollectNewStates(const TArray& InFiles, TMap& OutResults, + EFileState::Type FileState, ETreeState::Type TreeState, ELockState::Type LockState, + ERemoteState::Type RemoteState) { - return false; - } - - FGitSourceControlModule* GitSourceControl = FGitSourceControlModule::GetThreadSafe(); - if (!GitSourceControl) - { - return false; - } - FGitSourceControlProvider& Provider = GitSourceControl->GetProvider(); - const bool bUsingGitLfsLocking = Provider.UsesCheckout(); + if (InFiles.Num() == 0) + { + return false; + } - // 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(); + FGitState NewState; + NewState.FileState = FileState; + NewState.TreeState = TreeState; + NewState.LockState = LockState; + NewState.RemoteState = RemoteState; - for (const auto& Pair : InResults) - { - TSharedRef State = Provider.GetStateInternal(Pair.Key); - const FGitState& NewState = Pair.Value; - if (NewState.FileState != EFileState::Unset) + for (const auto& File : InFiles) { - // Invalid transition - if (NewState.FileState == EFileState::Added && !State->IsUnknown() && !State->CanAdd()) + FGitState& State = OutResults.FindOrAdd(File, NewState); + if (NewState.FileState != EFileState::Unset) { - continue; + State.FileState = NewState.FileState; } - State->State.FileState = NewState.FileState; - } - if (NewState.TreeState != ETreeState::Unset) - { - State->State.TreeState = NewState.TreeState; - } - // If we're updating lock state, also update user - if (NewState.LockState != ELockState::Unset) - { - State->State.LockState = NewState.LockState; - State->State.LockUser = NewState.LockUser; - } - if (NewState.RemoteState != ERemoteState::Unset) - { - State->State.RemoteState = NewState.RemoteState; - if (NewState.RemoteState == ERemoteState::UpToDate) + if (NewState.TreeState != ETreeState::Unset) { - State->State.HeadBranch = TEXT(""); + State.TreeState = NewState.TreeState; } - else + if (NewState.LockState != ELockState::Unset) { - State->State.HeadBranch = NewState.HeadBranch; + State.LockState = NewState.LockState; + } + if (NewState.RemoteState != ERemoteState::Unset) + { + State.RemoteState = NewState.RemoteState; } } - State->TimeStamp = Now; - // We've just updated the state, no need for UpdateStatus to be ran for this file again. - Provider.AddFileToIgnoreForceCache(State->LocalFilename); + return true; } - return true; -} - -bool CollectNewStates(const TMap& InStates, TMap& OutResults) -{ - if (InStates.Num() == 0) - { - return false; - } - - for (const auto& InState : InStates) + /** + * Helper struct for RemoveRedundantErrors() + */ + struct FRemoveRedundantErrors { - OutResults.Add(InState.Key, InState.Value.State); - } + FRemoveRedundantErrors(const FString& InFilter) : Filter(InFilter) {} - return true; -} + bool operator()(const FString& String) const + { + if (String.Contains(Filter)) + { + return true; + } -bool CollectNewStates(const TArray& InFiles, TMap& OutResults, EFileState::Type FileState, ETreeState::Type TreeState, ELockState::Type LockState, ERemoteState::Type RemoteState) -{ - if (InFiles.Num() == 0) - { - return false; - } + return false; + } - FGitState NewState; - NewState.FileState = FileState; - NewState.TreeState = TreeState; - NewState.LockState = LockState; - NewState.RemoteState = RemoteState; + /** The filter string we try to identify in the reported error */ + FString Filter; + }; - for (const auto& File : InFiles) + void RemoveRedundantErrors(FGitSourceControlCommand& InCommand, const FString& InFilter) { - FGitState& State = OutResults.FindOrAdd(File, NewState); - if (NewState.FileState != EFileState::Unset) - { - State.FileState = NewState.FileState; - } - if (NewState.TreeState != ETreeState::Unset) - { - State.TreeState = NewState.TreeState; - } - if (NewState.LockState != ELockState::Unset) + bool bFoundRedundantError = false; + for (auto Iter(InCommand.ResultInfo.ErrorMessages.CreateConstIterator()); Iter; Iter++) { - State.LockState = NewState.LockState; - } - if (NewState.RemoteState != ERemoteState::Unset) - { - State.RemoteState = NewState.RemoteState; + if (Iter->Contains(InFilter)) + { + InCommand.ResultInfo.InfoMessages.Add(*Iter); + bFoundRedundantError = true; + } } - } - - return true; -} -/** - * Helper struct for RemoveRedundantErrors() - */ -struct FRemoveRedundantErrors -{ - FRemoveRedundantErrors(const FString& InFilter) : Filter(InFilter) - {} + InCommand.ResultInfo.ErrorMessages.RemoveAll(FRemoveRedundantErrors(InFilter)); - bool operator()(const FString& String) const - { - if (String.Contains(Filter)) + // if we have no error messages now, assume success! + if (bFoundRedundantError && InCommand.ResultInfo.ErrorMessages.Num() == 0 && !InCommand.bCommandSuccessful) { - return true; + InCommand.bCommandSuccessful = true; } - - return false; } - /** The filter string we try to identify in the reported error */ - FString Filter; -}; + static TArray LockableTypes; -void RemoveRedundantErrors(FGitSourceControlCommand& InCommand, const FString& InFilter) -{ - bool bFoundRedundantError = false; - for (auto Iter(InCommand.ResultInfo.ErrorMessages.CreateConstIterator()); Iter; Iter++) + bool IsFileLFSLockable(const FString& InFile) { - if (Iter->Contains(InFilter)) + for (const auto& Type : LockableTypes) { - InCommand.ResultInfo.InfoMessages.Add(*Iter); - bFoundRedundantError = true; + if (InFile.EndsWith(Type)) + { + return true; + } } + return false; } - InCommand.ResultInfo.ErrorMessages.RemoveAll(FRemoveRedundantErrors(InFilter)); - - // if we have no error messages now, assume success! - if (bFoundRedundantError && InCommand.ResultInfo.ErrorMessages.Num() == 0 && !InCommand.bCommandSuccessful) + bool CheckLFSLockable(const FString& InPathToGitBinary, const FString& InRepositoryRoot, + const TArray& InFiles, TArray& OutErrorMessages) { - InCommand.bCommandSuccessful = true; - } -} - -static TArray LockableTypes; + TArray Results; + TArray Parameters; + Parameters.Add(TEXT("lockable")); // follow file renames -bool IsFileLFSLockable(const FString& InFile) -{ - for (const auto& Type : LockableTypes) - { - if (InFile.EndsWith(Type)) + const bool bResults = RunCommand(TEXT("check-attr"), InPathToGitBinary, InRepositoryRoot, Parameters, InFiles, + Results, OutErrorMessages); + if (!bResults) { - return true; + return false; } - } - return false; -} -bool CheckLFSLockable(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InFiles, TArray& OutErrorMessages) -{ - TArray Results; - TArray Parameters; - Parameters.Add(TEXT("lockable")); // follow file renames + for (int i = 0; i < InFiles.Num(); i++) + { + const FString& Result = Results[i]; + if (Result.EndsWith("set") && !Result.EndsWith("unset")) + { + const FString FileExt = InFiles[i].RightChop(1); // Remove wildcard (*) + LockableTypes.Add(FileExt); + } + } - const bool bResults = RunCommand(TEXT("check-attr"), InPathToGitBinary, InRepositoryRoot, Parameters, InFiles, Results, OutErrorMessages); - if (!bResults) - { - return false; + return true; } - for (int i = 0; i < InFiles.Num(); i++) + bool FetchRemote(const FString& InPathToGitBinary, const FString& InPathToRepositoryRoot, bool InUsingGitLfsLocking, + TArray& OutResults, TArray& OutErrorMessages) { - const FString& Result = Results[i]; - if (Result.EndsWith("set") && !Result.EndsWith("unset")) + // Force refresh lock states + if (InUsingGitLfsLocking) { - const FString FileExt = InFiles[i].RightChop(1); // Remove wildcard (*) - LockableTypes.Add(FileExt); + TMap Locks; + GetAllLocks(InPathToRepositoryRoot, InPathToGitBinary, OutErrorMessages, Locks, true); } - } + TArray Params {"--no-tags"}; + // fetch latest repo + // TODO specify branches? - return true; -} - -bool FetchRemote(const FString& InPathToGitBinary, const FString& InPathToRepositoryRoot, bool InUsingGitLfsLocking, TArray& OutResults, TArray& OutErrorMessages) -{ - // Force refresh lock states - if (InUsingGitLfsLocking) - { - TMap Locks; - GetAllLocks(InPathToRepositoryRoot, InPathToGitBinary, OutErrorMessages, Locks, true); + Params.Add(TEXT("--prune")); + return RunCommand(TEXT("fetch"), InPathToGitBinary, InPathToRepositoryRoot, Params, + FGitSourceControlModule::GetEmptyStringArray(), OutResults, OutErrorMessages); } - TArray Params{"--no-tags"}; - // fetch latest repo - // TODO specify branches? - Params.Add(TEXT("--prune")); - return RunCommand(TEXT("fetch"), InPathToGitBinary, InPathToRepositoryRoot, Params, - FGitSourceControlModule::GetEmptyStringArray(), OutResults, OutErrorMessages); -} - -bool PullOrigin(const FString& InPathToGitBinary, const FString& InPathToRepositoryRoot, const TArray& InFiles, TArray& OutFiles, - TArray& OutResults, TArray& OutErrorMessages) -{ - if (FGitSourceControlModule::Get().GetProvider().bPendingRestart) + bool PullOrigin(const FString& InPathToGitBinary, const FString& InPathToRepositoryRoot, + const TArray& InFiles, TArray& OutFiles, TArray& OutResults, + TArray& OutErrorMessages) { - FText PullFailMessage(LOCTEXT("Git_NeedBinariesUpdate_Msg", "Refused to Git Pull because your editor binaries are out of date.\n\n" - "Without a binaries update, new assets can become corrupted or cause crashes due to format " - "differences.\n\n" - "Please exit the editor, and update the project.")); - FText PullFailTitle(LOCTEXT("Git_NeedBinariesUpdate_Title", "Binaries Update Required")); + if (FGitSourceControlModule::Get().GetProvider().bPendingRestart) + { + FText PullFailMessage( + LOCTEXT("Git_NeedBinariesUpdate_Msg", + "Refused to Git Pull because your editor binaries are out of date.\n\n" + "Without a binaries update, new assets can become corrupted or cause crashes due to format " + "differences.\n\n" + "Please exit the editor, and update the project.")); + FText PullFailTitle(LOCTEXT("Git_NeedBinariesUpdate_Title", "Binaries Update Required")); #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 - FMessageDialog::Open(EAppMsgType::Ok, PullFailMessage, PullFailTitle); -#else - FMessageDialog::Open(EAppMsgType::Ok, PullFailMessage, &PullFailTitle); + FMessageDialog::Open(EAppMsgType::Ok, PullFailMessage, PullFailTitle); +#else + FMessageDialog::Open(EAppMsgType::Ok, PullFailMessage, &PullFailTitle); #endif - UE_LOG(LogSourceControl, Log, TEXT("Pull failed because we need a binaries update")); - return false; - } + UE_LOG(LogSourceControl, Log, TEXT("Pull failed because we need a binaries update")); + return false; + } - const TSet AlreadyReloaded {InFiles}; + const TSet AlreadyReloaded {InFiles}; - // Get remote branch - FString RemoteBranch; - if (!GetRemoteBranchName(InPathToGitBinary, InPathToRepositoryRoot, RemoteBranch)) - { - // No remote to sync from - return false; - } + // Get remote branch + FString RemoteBranch; + if (!GetRemoteBranchName(InPathToGitBinary, InPathToRepositoryRoot, RemoteBranch)) + { + // No remote to sync from + return false; + } - // Get the list of files which will be updated (either ones we changed locally, which will get potentially rebased or merged, or the remote ones that will update) - TArray DifferentFiles; - const bool bResultDiff = RunCommand(TEXT("diff"), InPathToGitBinary, InPathToRepositoryRoot, { TEXT("--name-only"), RemoteBranch }, FGitSourceControlModule::GetEmptyStringArray(), DifferentFiles, OutErrorMessages); - if (!bResultDiff) - { - return false; - } + // Get the list of files which will be updated (either ones we changed locally, which will get potentially + // rebased or merged, or the remote ones that will update) + TArray DifferentFiles; + const bool bResultDiff = + RunCommand(TEXT("diff"), InPathToGitBinary, InPathToRepositoryRoot, {TEXT("--name-only"), RemoteBranch}, + FGitSourceControlModule::GetEmptyStringArray(), DifferentFiles, OutErrorMessages); + if (!bResultDiff) + { + return false; + } - // Nothing to pull - if (!DifferentFiles.Num()) - { - return true; - } + // Nothing to pull + if (!DifferentFiles.Num()) + { + return true; + } - const TArray& AbsoluteDifferentFiles = AbsoluteFilenames(DifferentFiles, InPathToRepositoryRoot); + const TArray& AbsoluteDifferentFiles = AbsoluteFilenames(DifferentFiles, InPathToRepositoryRoot); - if (AlreadyReloaded.Num()) - { - OutFiles.Reserve(AbsoluteDifferentFiles.Num() - AlreadyReloaded.Num()); - for (const auto& File : AbsoluteDifferentFiles) + if (AlreadyReloaded.Num()) { - if (!AlreadyReloaded.Contains(File)) + OutFiles.Reserve(AbsoluteDifferentFiles.Num() - AlreadyReloaded.Num()); + for (const auto& File : AbsoluteDifferentFiles) { - OutFiles.Add(File); + if (!AlreadyReloaded.Contains(File)) + { + OutFiles.Add(File); + } } } - } - else - { - OutFiles.Append(AbsoluteDifferentFiles); - } + else + { + OutFiles.Append(AbsoluteDifferentFiles); + } - TArray Files; - for (const auto& File : OutFiles) - { - if (IsFileLFSLockable(File)) + TArray Files; + for (const auto& File : OutFiles) { - Files.Add(File); + if (IsFileLFSLockable(File)) + { + Files.Add(File); + } } - } - const bool bShouldReload = Files.Num() > 0; - TArray PackagesToReload; - if (bShouldReload) - { - const auto PackagesToReloadResult = Async(EAsyncExecution::TaskGraphMainThread, [=] { - return UnlinkPackages(Files); - }); - PackagesToReload = PackagesToReloadResult.Get(); - } + const bool bShouldReload = Files.Num() > 0; + TArray PackagesToReload; + if (bShouldReload) + { + const auto PackagesToReloadResult = + Async(EAsyncExecution::TaskGraphMainThread, [=] { return UnlinkPackages(Files); }); + PackagesToReload = PackagesToReloadResult.Get(); + } - // Reset HEAD and index to remote - TArray InfoMessages; - bool bSuccess = RunCommand(TEXT("pull"), InPathToGitBinary, InPathToRepositoryRoot, { "--rebase", "--autostash" }, FGitSourceControlModule::GetEmptyStringArray(), - InfoMessages, OutErrorMessages); + // Reset HEAD and index to remote + TArray InfoMessages; + bool bSuccess = RunCommand(TEXT("pull"), InPathToGitBinary, InPathToRepositoryRoot, {"--rebase", "--autostash"}, + FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, OutErrorMessages); - if (bShouldReload) - { - const auto ReloadPackagesResult = Async(EAsyncExecution::TaskGraphMainThread, [=] { - TArray Packages = PackagesToReload; - ReloadPackages(Packages); - }); - ReloadPackagesResult.Wait(); + if (bShouldReload) + { + const auto ReloadPackagesResult = Async(EAsyncExecution::TaskGraphMainThread, [=] { + TArray Packages = PackagesToReload; + ReloadPackages(Packages); + }); + ReloadPackagesResult.Wait(); + } + + return bSuccess; } - return bSuccess; -} + TSharedPtr GetOriginRevisionOnBranch(const FString& InPathToGitBinary, + const FString& InRepositoryRoot, + const FString& InRelativeFileName, + TArray& OutErrorMessages, + const FString& BranchName) + { + TGitSourceControlHistory OutHistory; -TSharedPtr GetOriginRevisionOnBranch( const FString & InPathToGitBinary, const FString & InRepositoryRoot, const FString & InRelativeFileName, TArray & OutErrorMessages, const FString & BranchName ) -{ - TGitSourceControlHistory OutHistory; + TArray Results; + TArray Parameters; + Parameters.Add(BranchName); + Parameters.Add(TEXT("--date=raw")); + Parameters.Add(TEXT("--pretty=medium")); // make sure format matches expected in ParseLogResults - TArray< FString > Results; - TArray< FString > Parameters; - Parameters.Add( BranchName ); - Parameters.Add( TEXT( "--date=raw" ) ); - Parameters.Add( TEXT( "--pretty=medium" ) ); // make sure format matches expected in ParseLogResults + TArray Files; + const auto bResults = + RunCommand(TEXT("show"), InPathToGitBinary, InRepositoryRoot, Parameters, Files, Results, OutErrorMessages); - TArray< FString > Files; - const auto bResults = RunCommand( TEXT( "show" ), InPathToGitBinary, InRepositoryRoot, Parameters, Files, Results, OutErrorMessages ); + if (bResults) + { + ParseLogResults(Results, OutHistory); + } - if ( bResults ) - { - ParseLogResults( Results, OutHistory ); - } + if (OutHistory.Num() > 0) + { + auto AbsoluteFileName = FPaths::ConvertRelativePathToFull(InRelativeFileName); - if ( OutHistory.Num() > 0 ) - { - auto AbsoluteFileName = FPaths::ConvertRelativePathToFull( InRelativeFileName ); + AbsoluteFileName.RemoveFromStart(InRepositoryRoot); - AbsoluteFileName.RemoveFromStart( InRepositoryRoot ); + if (AbsoluteFileName[0] == '/') + { + AbsoluteFileName.RemoveAt(0); + } - if ( AbsoluteFileName[ 0 ] == '/' ) - { - AbsoluteFileName.RemoveAt( 0 ); - } + OutHistory[0]->Filename = AbsoluteFileName; - OutHistory[ 0 ]->Filename = AbsoluteFileName; + return OutHistory[0]; + } - return OutHistory[ 0 ]; + return nullptr; } - return nullptr; -} - } // namespace GitSourceControlUtils #undef LOCTEXT_NAMESPACE diff --git a/Source/GitSourceControl/Private/LFSLockProvider.cpp b/Source/GitSourceControl/Private/LFSLockProvider.cpp index b6d14816..b9ec228b 100644 --- a/Source/GitSourceControl/Private/LFSLockProvider.cpp +++ b/Source/GitSourceControl/Private/LFSLockProvider.cpp @@ -1,9 +1,59 @@ #include "LFSLockProvider.h" +#include "GitSourceControlUtils.h" bool ULFSLockProvider::RunLFSCommand(const FString& InCommand, const FString& InRepositoryRoot, const FString& GitBinaryFallback, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages) { - return false; + FString Command = InCommand; +#if GIT_USE_CUSTOM_LFS + FString BaseDir = IPluginManager::Get().FindPlugin("GitSourceControl")->GetBaseDir(); + #if PLATFORM_WINDOWS + FString LFSLockBinary = FString::Printf(TEXT("%s/git-lfs.exe"), *BaseDir); + #elif PLATFORM_MAC + #if ENGINE_MAJOR_VERSION >= 5 + #if PLATFORM_MAC_ARM64 + FString LFSLockBinary = FString::Printf(TEXT("%s/git-lfs-mac-arm64"), *BaseDir); + #else + FString LFSLockBinary = FString::Printf(TEXT("%s/git-lfs-mac-amd64"), *BaseDir); + #endif + #else + FString LFSLockBinary = FString::Printf(TEXT("%s/git-lfs-mac-amd64"), *BaseDir); + #endif + #elif PLATFORM_LINUX + FString LFSLockBinary = FString::Printf(TEXT("%s/git-lfs"), *BaseDir); + #else + ensureMsgf(false, TEXT("Unhandled platform for LFS binary!")); + const FString& LFSLockBinary = GitBinaryFallback; + Command = TEXT("lfs ") + Command; + #endif +#else + const FString& LFSLockBinary = GitBinaryFallback; + Command = TEXT("lfs ") + Command; +#endif + + return GitSourceControlUtils::RunCommand(Command, LFSLockBinary, InRepositoryRoot, InParameters, InFiles, + OutResults, OutErrorMessages); +} + +bool ULFSLockProvider::GetLockedFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutResults, TArray& OutErrorMessages) +{ + return RunLFSCommand(TEXT("locks"), InRepositoryRoot, Params.GitBinaryPath, Params.CustomParams, Params.FileNames, + OutResults, OutErrorMessages); +} + +bool ULFSLockProvider::LockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutResults, TArray& OutErrorMessages) +{ + return RunLFSCommand(TEXT("lock"), InRepositoryRoot, Params.GitBinaryPath, Params.CustomParams, Params.FileNames, + OutResults, OutErrorMessages); +} + +bool ULFSLockProvider::UnlockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutResults, TArray& OutErrorMessages) +{ + return RunLFSCommand(TEXT("unlock"), InRepositoryRoot, Params.GitBinaryPath, Params.CustomParams, Params.FileNames, + OutResults, OutErrorMessages); } diff --git a/Source/GitSourceControl/Private/LFSLockProvider.h b/Source/GitSourceControl/Private/LFSLockProvider.h index 30a6df82..5fcca02f 100644 --- a/Source/GitSourceControl/Private/LFSLockProvider.h +++ b/Source/GitSourceControl/Private/LFSLockProvider.h @@ -13,4 +13,13 @@ class ULFSLockProvider : public UGitLockProviderBase const FString& GitBinaryFallback, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages) override; + + virtual bool GetLockedFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutResults, TArray& OutErrorMessages) override; + + virtual bool LockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutResults, TArray& OutErrorMessages) override; + + virtual bool UnlockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutResults, TArray& OutErrorMessages) override; }; diff --git a/Source/GitSourceControl/Private/ModioLockProvider.cpp b/Source/GitSourceControl/Private/ModioLockProvider.cpp new file mode 100644 index 00000000..1573fc9a --- /dev/null +++ b/Source/GitSourceControl/Private/ModioLockProvider.cpp @@ -0,0 +1,156 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "ModioLockProvider.h" +#include "Async/TaskGraphInterfaces.h" +#include "Containers/Ticker.h" +#include "Framework/Application/SlateApplication.h" +#include "GitSourceControlModule.h" +#include "HAL/PlatformProcess.h" +#include "HttpModule.h" +#include "ISourceControlModule.h" +#include "Interfaces/IHttpRequest.h" +#include "Interfaces/IHttpResponse.h" +#include "Interfaces/IProjectManager.h" +#include "Misc/App.h" +#include "Misc/EngineVersionComparison.h" +#include "Misc/Optional.h" + +TSharedRef UModioLockProvider::GetLocksRequest() +{ + FHttpModule& HttpModule = FHttpModule::Get(); + + TSharedRef Request = HttpModule.CreateRequest(); + FString RequestURL = FString("ServerIP") + TEXT("/api/FileLock/lock"); + Request->SetVerb(TEXT("GET")); + Request->SetURL(RequestURL); + return Request; +} + +TSharedRef UModioLockProvider::LockFileRequest(const FString& Username, + const FString& FilePath, + const FString& ProjectName) +{ + FHttpModule& HttpModule = FHttpModule::Get(); + + TSharedRef Request = HttpModule.CreateRequest(); + FString RequestURL = FString("ServerIP") + TEXT("/api/FileLock/lock"); + Request->SetVerb(TEXT("POST")); + Request->SetURL(RequestURL); + Request->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + FString RequestContent = FString::Format(TEXT("{\"username\": \"{0}\", \"assetPath\": \"{1}\", \"projectName\": " + "\"{2}\", \"bCreateProject\": \"true\" }"), + {*Username, *FilePath, *ProjectName}); + Request->SetContentAsString(RequestContent); + return Request; +} + +TSharedRef UModioLockProvider::UnlockFileRequest(const FString& Username, + const FString& FilePath, + const FString& ProjectName) +{ + FHttpModule& HttpModule = FHttpModule::Get(); + + TSharedRef Request = HttpModule.CreateRequest(); + FString RequestURL = FString("ServerIP") + TEXT("/api/FileLock/lock"); + Request->SetVerb(TEXT("DELETE")); + Request->SetURL(RequestURL); + Request->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + FString RequestContent = FString::Format(TEXT("{\"username\": \"{0}\", \"assetPath\": \"{1}\", \"projectName\": " + "\"{2}\" }"), + {*Username, *FilePath, *ProjectName}); + Request->SetContentAsString(RequestContent); + return Request; +} + +TUnion UModioLockProvider::PerformHttpRequest( + TSharedRef Request) +{ + TUnion Result; + bool bRequestDone = false; + Request->OnProcessRequestComplete().BindLambda( + [&](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bConnectedSuccessfully) { + if (bConnectedSuccessfully) + { + Result.SetSubtype(Response->GetContentAsString()); + } + else + { + Result.SetSubtype(Request->GetStatus()); + } + }); + Request->ProcessRequest(); + while (!bRequestDone) + { + YieldThread(); + } + return Result; +} + +void UModioLockProvider::YieldThread() +{ + FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread); +#if UE_VERSION_OLDER_THAN(5, 3, 0) + FTicker::GetCoreTicker().Tick(FApp::GetDeltaTime()); +#else + FTSTicker::GetCoreTicker().Tick(FApp::GetDeltaTime()); +#endif + FSlateApplication::Get().PumpMessages(); + FSlateApplication::Get().Tick(); + FPlatformProcess::Sleep(0); +} + +bool UModioLockProvider::RunLFSCommand(const FString& InCommand, const FString& InRepositoryRoot, + const FString& GitBinaryFallback, const TArray& InParameters, + const TArray& InFiles, TArray& OutResults, + TArray& OutErrorMessages) +{ + UE_LOG(LogSourceControl, Display, TEXT("Raw LFS command invoked on provider which does not support it")); + return false; +} + +bool UModioLockProvider::GetLockedFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutResults, TArray& OutErrorMessages) +{ + auto Result = PerformHttpRequest(GetLocksRequest()); + if (Result.GetCurrentSubtypeIndex() == 0) + { + // deserialize here + return true; + } + else + { + return false; + } +} + +bool UModioLockProvider::LockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutResults, TArray& OutErrorMessages) +{ + auto Result = PerformHttpRequest(LockFileRequest(FGitSourceControlModule::Get().GetProvider().GetLockUser(), + Params.FileNames[0], FApp::GetProjectName())); + if (Result.GetCurrentSubtypeIndex() == 0) + { + // deserialize here + return true; + } + else + { + return false; + } +} + +bool UModioLockProvider::UnlockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutResults, TArray& OutErrorMessages) +{ + auto Result = PerformHttpRequest(UnlockFileRequest(FGitSourceControlModule::Get().GetProvider().GetLockUser(), + Params.FileNames[0], FApp::GetProjectName())); + if (Result.GetCurrentSubtypeIndex() == 0) + { + // deserialize here + return true; + } + else + { + return false; + } +} diff --git a/Source/GitSourceControl/Private/ModioLockProvider.h b/Source/GitSourceControl/Private/ModioLockProvider.h new file mode 100644 index 00000000..0b0a97cb --- /dev/null +++ b/Source/GitSourceControl/Private/ModioLockProvider.h @@ -0,0 +1,50 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "Containers/Union.h" +#include "CoreMinimal.h" +#include "IGitLockProvider.h" +#include "Templates/SharedPointer.h" +#include "UObject/NoExportTypes.h" + +#include "ModioLockProvider.generated.h" + +namespace EHttpRequestStatus +{ + enum Type; +} +/** + * + */ +UCLASS() +class UModioLockProvider : public UObject, public IGitLockProvider +{ + GENERATED_BODY() + TSharedRef GetLocksRequest(); + TSharedRef LockFileRequest(const FString& Username, + const FString& FilePath, + const FString& ProjectName); + TSharedRef UnlockFileRequest(const FString& Username, + const FString& FilePath, + const FString& ProjectName); + + TUnion PerformHttpRequest( + TSharedRef Request); + + void YieldThread(); + +public: + bool RunLFSCommand(const FString& InCommand, const FString& InRepositoryRoot, const FString& GitBinaryFallback, + const TArray& InParameters, const TArray& InFiles, TArray& OutResults, + TArray& OutErrorMessages) override; + + bool GetLockedFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutResults, TArray& OutErrorMessages) override; + + bool LockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, TArray& OutResults, + TArray& OutErrorMessages) override; + + bool UnlockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, TArray& OutResults, + TArray& OutErrorMessages) override; +}; diff --git a/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp b/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp index fc4d150d..0c9428d6 100644 --- a/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp +++ b/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp @@ -683,7 +683,7 @@ const UClass* SGitSourceControlSettings::GetLockProviderClass() const void SGitSourceControlSettings::SetLockProviderClass(const UClass* Value) { FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); - GitSourceControl.AccessSettings().SetLockProviderClass(Value); + GitSourceControl.SetLockProviderClass(Value); } EVisibility SGitSourceControlSettings::MustInitializeGitRepository() const diff --git a/Source/GitSourceControl/Public/GitSourceControlModule.h b/Source/GitSourceControl/Public/GitSourceControlModule.h index c0fb390f..214fa248 100644 --- a/Source/GitSourceControl/Public/GitSourceControlModule.h +++ b/Source/GitSourceControl/Public/GitSourceControlModule.h @@ -8,8 +8,8 @@ #include "Modules/ModuleInterface.h" #include "Modules/ModuleManager.h" -#include "GitSourceControlSettings.h" #include "GitSourceControlProvider.h" +#include "GitSourceControlSettings.h" struct FAssetData; class FExtender; @@ -40,7 +40,7 @@ Written and contributed by Sebastien Rombauts (sebastien.rombauts@gmail.com) - Git LFS 2 File Locking is working with Git 2.10+ and Git LFS 2.0.0 - Windows, Mac and Linux -### TODO +### TODO 1. configure the name of the remote instead of default "origin" ### TODO LFS 2.x File Locking @@ -51,13 +51,16 @@ Known issues: 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 ???) + 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" + - 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 @@ -68,14 +71,17 @@ Use "TODO LFS" in the code to track things left to do/improve/refactor: - 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 +- 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 */ @@ -103,7 +109,7 @@ class FGitSourceControlModule : public IModuleInterface return GitSourceControlProvider; } - GITSOURCECONTROL_API static const TArray< FString > & GetEmptyStringArray() + GITSOURCECONTROL_API static const TArray& GetEmptyStringArray() { return EmptyStringArray; } @@ -116,7 +122,7 @@ class FGitSourceControlModule : public IModuleInterface */ static inline FGitSourceControlModule& Get() { - return FModuleManager::Get().LoadModuleChecked< FGitSourceControlModule >("GitSourceControl"); + return FModuleManager::Get().LoadModuleChecked("GitSourceControl"); } static inline FGitSourceControlModule* GetThreadSafe() @@ -134,11 +140,17 @@ class FGitSourceControlModule : public IModuleInterface /** Set list of error messages that occurred after last git command */ static void SetLastErrors(const TArray& InErrors); + UGitLockProviderBase* GetLockProvider() const; + void SetLockProviderClass(TSoftClassPtr Provider); + private: TSharedRef OnExtendContentBrowserAssetSelectionMenu(const TArray& SelectedAssets); void CreateGitContentBrowserAssetMenu(FMenuBuilder& MenuBuilder, const TArray SelectedAssets); void DiffAssetAgainstGitOriginBranch(const TArray SelectedAssets, FString BranchName) const; - void DiffAgainstOriginBranch(UObject* InObject, const FString& InPackagePath, const FString& InPackageName, const FString& BranchName) const; + void DiffAgainstOriginBranch(UObject* InObject, const FString& InPackagePath, const FString& InPackageName, + const FString& BranchName) const; + + void UpdateLockProviderInstance(); /** The one and only Git revision control provider */ FGitSourceControlProvider GitSourceControlProvider; @@ -146,6 +158,8 @@ class FGitSourceControlModule : public IModuleInterface /** The settings for Git revision control */ FGitSourceControlSettings GitSourceControlSettings; + TStrongObjectPtr LockProvider; + static TArray EmptyStringArray; #if ENGINE_MAJOR_VERSION >= 5 diff --git a/Source/GitSourceControl/Public/GitSourceControlSettings.h b/Source/GitSourceControl/Public/GitSourceControlSettings.h index 3c1a3576..b5cab2ff 100644 --- a/Source/GitSourceControl/Public/GitSourceControlSettings.h +++ b/Source/GitSourceControl/Public/GitSourceControlSettings.h @@ -12,7 +12,7 @@ class GITSOURCECONTROL_API FGitSourceControlSettings { public: /** Get the Git Binary Path */ - const FString & GetBinaryPath() const; + const FString& GetBinaryPath() const; /** Set the Git Binary Path */ bool SetBinaryPath(const FString& InString); @@ -31,8 +31,6 @@ class GITSOURCECONTROL_API FGitSourceControlSettings const TSoftClassPtr GetLockProviderClass() const; - bool SetLockProviderClass(TSoftClassPtr Provider); - /** Load settings from ini file */ void LoadSettings(); @@ -40,6 +38,9 @@ class GITSOURCECONTROL_API FGitSourceControlSettings void SaveSettings() const; private: + friend class FGitSourceControlModule; + bool SetLockProviderClass(TSoftClassPtr Provider); + /** A critical section for settings access */ mutable FCriticalSection CriticalSection; diff --git a/Source/GitSourceControl/Public/GitSourceControlUtils.h b/Source/GitSourceControl/Public/GitSourceControlUtils.h index 30e21dc6..eac220f8 100644 --- a/Source/GitSourceControl/Public/GitSourceControlUtils.h +++ b/Source/GitSourceControl/Public/GitSourceControlUtils.h @@ -19,7 +19,6 @@ class FGitSourceControlCommand; class FGitScopedTempFile { public: - /** Constructor - open & write string to temp file */ FGitScopedTempFile(const FText& InText); @@ -41,343 +40,382 @@ class FGitLockedFilesCache public: static FDateTime LastUpdated; - static const TMap& GetLockedFiles() { return LockedFiles; } - static void SetLockedFiles(const TMap& newLocks); - static void AddLockedFile(const FString& filePath, const FString& lockUser); - static void RemoveLockedFile(const FString& filePath); + static const TMap& GetLockedFiles() + { + return LockedFiles; + } + static void SetLockedFiles(const TMap& 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 void OnFileLockChanged(const FString& filePath, const FString& lockUser, bool locked); + // update local read/write state when our own lock statuses change static TMap 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) - */ + * 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(TArray& 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(FString & AbsoluteFilePath, 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(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(); + /** + * 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); + /** + * 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); + /** + * 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 - */ + * 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 - */ + * 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); + /** + * 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 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 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 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& 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 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& OutBranchNames); -/** - * 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); + /** + * 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); -/** - * 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 - */ -GITSOURCECONTROL_API 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& InParameters, const TArray& InFiles, FString& OutResults, FString& OutErrors, const int32 ExpectedReturnCode = 0); + /** + * 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); -/** - * Unloads packages of specified named files - */ -TArray UnlinkPackages(const TArray& InPackageNames); + /** + * 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 + */ + GITSOURCECONTROL_API bool RunCommand(const FString& InCommand, const FString& InPathToGitBinary, + const FString& InRepositoryRoot, const TArray& InParameters, + const TArray& InFiles, TArray& OutResults, + TArray& OutErrorMessages); + bool RunCommandInternalRaw(const FString& InCommand, const FString& InPathToGitBinary, + const FString& InRepositoryRoot, const TArray& InParameters, + const TArray& InFiles, FString& OutResults, FString& OutErrors, + const int32 ExpectedReturnCode = 0); -/** - * Reloads packages for these packages - */ -void ReloadPackages(TArray& InPackagesToReload); + /** + * Unloads packages of specified named files + */ + TArray UnlinkPackages(const TArray& InPackageNames); -/** - * Gets all Git tracked files, including within directories, recursively - */ -bool ListFilesInDirectoryRecurse(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InDirectory, TArray& OutFiles); + /** + * Reloads packages for these packages + */ + void ReloadPackages(TArray& InPackagesToReload); -/** - * 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& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages); + /** + * Gets all Git tracked files, including within directories, recursively + */ + bool ListFilesInDirectoryRecurse(const FString& InPathToGitBinary, const FString& InRepositoryRoot, + const FString& InDirectory, TArray& OutFiles); -/** - * @brief Detects how to parse the result of a "status" command to get workspace file states - * - * It is either a command for a whole directory (ie. "Content/", in case of "Submit to Revision Control" menu), - * or for one or more files all on a same directory (by design, since we group files by directory in RunUpdateStatus()) - * - * @param[in] InPathToGitBinary The path to the Git binary - * @param[in] InRepositoryRoot The Git repository from where to run the command - usually the Game directory (can be empty) - * @param[in] InUsingLfsLocking Tells if using the Git LFS file Locking workflow - * @param[in] InFiles List of files in a directory, or the path to the directory itself (never empty). - * @param[out] InResults Results from the "status" command - * @param[out] OutStates States of files for witch the status has been gathered (distinct than InFiles in case of a "directory status") - */ -GITSOURCECONTROL_API void ParseStatusResults( const FString & InPathToGitBinary, const FString & InRepositoryRoot, const bool InUsingLfsLocking, const TArray< FString > & InFiles, const TMap< FString, FString > & InResults, TMap< FString, FGitSourceControlState > & OutStates ); + /** + * 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& InParameters, const TArray& InFiles, TArray& OutResults, + TArray& 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& Files, - TArray& OutErrorMessages, TMap& OutStates); + /** + * @brief Detects how to parse the result of a "status" command to get workspace file states + * + * It is either a command for a whole directory (ie. "Content/", in case of "Submit to Revision Control" menu), + * or for one or more files all on a same directory (by design, since we group files by directory in + * RunUpdateStatus()) + * + * @param[in] InPathToGitBinary The path to the Git binary + * @param[in] InRepositoryRoot The Git repository from where to run the command - usually the Game directory + * (can be empty) + * @param[in] InUsingLfsLocking Tells if using the Git LFS file Locking workflow + * @param[in] InFiles List of files in a directory, or the path to the directory itself (never empty). + * @param[out] InResults Results from the "status" command + * @param[out] OutStates States of files for witch the status has been gathered (distinct than InFiles in + * case of a "directory status") + */ + GITSOURCECONTROL_API void ParseStatusResults(const FString& InPathToGitBinary, const FString& InRepositoryRoot, + const bool InUsingLfsLocking, const TArray& InFiles, + const TMap& InResults, + TMap& 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& InFiles, + /** + * 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& Files, TArray& OutErrorMessages, TMap& 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& OutErrorMessages, TGitSourceControlHistory& OutHistory); + /** + * 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& InFiles, + TArray& OutErrorMessages, TMap& OutStates); -/** - * 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 RelativeFilenames(const TArray& InFileNames, const FString& InRelativeTo); + /** + * 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); -/** - * 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 AbsoluteFilenames(const TArray& InFileNames, const FString& InRelativeTo); + /** + * Keep Consistency of being file staged with simple argument + * + * @param Filename Saved filename + */ + bool UpdateFileStagingOnSavedInternal(const FString& Filename); -/** - * 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); + /** + * + * + * @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); - bool RunLFSCommand(const FString& InCommand, const FString& InRepositoryRoot, const FString& GitBinaryFallback, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages); + /** + * + * + * @param Filename Saved filename + * @param Pkg Package (for adapting delegate) + * @param ObjectSaveContext Context for save (for adapting delegate) + */ + bool UpdateChangelistStateByCommand(); -/** - * Helper function for various commands to update cached states. - * @returns true if any states were updated - */ -GITSOURCECONTROL_API bool UpdateCachedStates( const TMap< const FString, FGitState > & InResults ); + /** + * 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); -/** -* Helper function for various commands to collect new states. -* @returns true if any states were updated -*/ -GITSOURCECONTROL_API 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& InFiles, TMap& OutResults, EFileState::Type FileState, ETreeState::Type TreeState = ETreeState::Unset, ELockState::Type LockState = ELockState::Unset, ERemoteState::Type RemoteState = ERemoteState::Unset); + /** + * 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& OutErrorMessages, TGitSourceControlHistory& OutHistory); /** - * 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& OutErrorMessages, TMap& OutLocks, bool bInvalidateCache = false); + * 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 RelativeFilenames(const TArray& InFileNames, const FString& InRelativeTo); -/** - * Gets locks from state cache - */ -void GetLockedFiles(const TArray& InFiles, TArray& OutFiles); + /** + * 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 AbsoluteFilenames(const TArray& InFileNames, const FString& InRelativeTo); -/** - * Checks cache for if this file type is lockable - */ -bool IsFileLFSLockable(const FString& InFile); + /** + * 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); -/** - * Gets Git attribute to see if these extensions are lockable - */ -bool CheckLFSLockable(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InFiles, TArray& OutErrorMessages); + /** + * Helper function for various commands to update cached states. + * @returns true if any states were updated + */ + GITSOURCECONTROL_API bool UpdateCachedStates(const TMap& InResults); + + /** + * Helper function for various commands to collect new states. + * @returns true if any states were updated + */ + GITSOURCECONTROL_API bool CollectNewStates(const TMap& InStates, + TMap& OutResults); + + /** + * Helper function for various commands to collect new states. + * @returns true if any states were updated + */ + bool CollectNewStates(const TArray& InFiles, TMap& 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& OutErrorMessages, TMap& OutLocks, + bool bInvalidateCache = false); + + /** + * Gets locks from state cache + */ + void GetLockedFiles(const TArray& InFiles, TArray& OutFiles); -GITSOURCECONTROL_API bool FetchRemote( const FString & InPathToGitBinary, const FString & InPathToRepositoryRoot, bool InUsingGitLfsLocking, TArray< FString > & OutResults, TArray< FString > & OutErrorMessages ); + /** + * 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& InFiles, TArray& OutErrorMessages); -bool PullOrigin(const FString& InPathToGitBinary, const FString& InPathToRepositoryRoot, const TArray& InFiles, TArray& OutFiles, - TArray& OutResults, TArray& OutErrorMessages); + GITSOURCECONTROL_API bool FetchRemote(const FString& InPathToGitBinary, const FString& InPathToRepositoryRoot, + bool InUsingGitLfsLocking, TArray& OutResults, + TArray& OutErrorMessages); + bool PullOrigin(const FString& InPathToGitBinary, const FString& InPathToRepositoryRoot, + const TArray& InFiles, TArray& OutFiles, TArray& OutResults, + TArray& OutErrorMessages); -GITSOURCECONTROL_API TSharedPtr< class ISourceControlRevision, ESPMode::ThreadSafe > GetOriginRevisionOnBranch( const FString & InPathToGitBinary, const FString & InRepositoryRoot, const FString & InRelativeFileName, TArray< FString > & OutErrorMessages, const FString & BranchName ); + GITSOURCECONTROL_API TSharedPtr GetOriginRevisionOnBranch( + const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InRelativeFileName, + TArray& OutErrorMessages, const FString& BranchName); -} +} // namespace GitSourceControlUtils diff --git a/Source/GitSourceControl/Public/IGitLockProvider.h b/Source/GitSourceControl/Public/IGitLockProvider.h index 1fe39623..935c852b 100644 --- a/Source/GitSourceControl/Public/IGitLockProvider.h +++ b/Source/GitSourceControl/Public/IGitLockProvider.h @@ -8,6 +8,14 @@ class UGitLockProvider : public UInterface GENERATED_BODY() }; +struct FGitFileLockOpParams +{ + FString GitBinaryPath; + TArray CustomParams; + bool bUseLocalCache; + TArray FileNames; +}; + class IGitLockProvider { GENERATED_BODY() @@ -16,9 +24,15 @@ class IGitLockProvider const FString& GitBinaryFallback, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages) = 0; + virtual bool GetLockedFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutResults, TArray& OutErrorMessages) = 0; + virtual bool LockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutResults, TArray& OutErrorMessages) = 0; + virtual bool UnlockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutResults, TArray& OutErrorMessages) = 0; }; -UCLASS() +UCLASS(Abstract) class UGitLockProviderBase : public UObject, public IGitLockProvider { GENERATED_BODY() @@ -29,4 +43,22 @@ class UGitLockProviderBase : public UObject, public IGitLockProvider { return false; } + + bool GetLockedFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutResults, TArray& OutErrorMessages) override + { + return false; + } + + bool LockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, TArray& OutResults, + TArray& OutErrorMessages) override + { + return false; + } + + bool UnlockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, TArray& OutResults, + TArray& OutErrorMessages) override + { + return false; + } }; From af39a433c2ababe4ff8ac78d301cc3b9b0f0d420 Mon Sep 17 00:00:00 2001 From: Stephen Whittle Date: Tue, 11 Feb 2025 09:58:13 +1100 Subject: [PATCH 05/11] Lock provider now deserializes HTTP response when necessary --- .../GitSourceControl.Build.cs | 3 +- .../Private/ModioLockProvider.cpp | 53 +++++++++++++++++-- .../Private/ModioLockProvider.h | 10 ++-- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/Source/GitSourceControl/GitSourceControl.Build.cs b/Source/GitSourceControl/GitSourceControl.Build.cs index 6049ca3c..96d6e4bd 100644 --- a/Source/GitSourceControl/GitSourceControl.Build.cs +++ b/Source/GitSourceControl/GitSourceControl.Build.cs @@ -23,7 +23,8 @@ public GitSourceControl(ReadOnlyTargetRules Target) : base(Target) "SourceControlWindows", "Projects", "PropertyEditor", - "HTTP" + "HTTP", + "Json" } ); diff --git a/Source/GitSourceControl/Private/ModioLockProvider.cpp b/Source/GitSourceControl/Private/ModioLockProvider.cpp index 1573fc9a..c25770cd 100644 --- a/Source/GitSourceControl/Private/ModioLockProvider.cpp +++ b/Source/GitSourceControl/Private/ModioLockProvider.cpp @@ -14,6 +14,7 @@ #include "Misc/App.h" #include "Misc/EngineVersionComparison.h" #include "Misc/Optional.h" +#include "Serialization/JsonSerializer.h" TSharedRef UModioLockProvider::GetLocksRequest() { @@ -62,10 +63,9 @@ TSharedRef UModioLockProvider::UnlockFi return Request; } -TUnion UModioLockProvider::PerformHttpRequest( - TSharedRef Request) +TUnion UModioLockProvider::PerformHttpRequest(TSharedRef Request) { - TUnion Result; + TUnion Result; bool bRequestDone = false; Request->OnProcessRequestComplete().BindLambda( [&](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bConnectedSuccessfully) { @@ -75,7 +75,7 @@ TUnion UModioLockProvider::PerformHttpRequest } else { - Result.SetSubtype(Request->GetStatus()); + Result.SetSubtype(Response->GetResponseCode()); } }); Request->ProcessRequest(); @@ -86,6 +86,34 @@ TUnion UModioLockProvider::PerformHttpRequest return Result; } +TSharedPtr UModioLockProvider::GetResponseAsJsonObject(const FString& ResponseString) +{ + TSharedPtr TopLevelJson = FJsonStringReader::Create(ResponseString); + TSharedPtr ParsedResponse; + if (!FJsonSerializer::Deserialize(*TopLevelJson, ParsedResponse, FJsonSerializer::EFlags::None)) + { + return nullptr; + } + else + { + return ParsedResponse; + } +} + +TArray> UModioLockProvider::GetResponseAsJsonArray(const FString& ResponseString) +{ + TSharedPtr TopLevelJson = FJsonStringReader::Create(ResponseString); + TArray> ParsedResponse; + if (!FJsonSerializer::Deserialize(*TopLevelJson, ParsedResponse, FJsonSerializer::EFlags::None)) + { + return {}; + } + else + { + return ParsedResponse; + } +} + void UModioLockProvider::YieldThread() { FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread); @@ -114,11 +142,22 @@ bool UModioLockProvider::GetLockedFiles(const FString& InRepositoryRoot, const F auto Result = PerformHttpRequest(GetLocksRequest()); if (Result.GetCurrentSubtypeIndex() == 0) { - // deserialize here + TArray> ResponseJSON = GetResponseAsJsonArray(Result.GetSubtype()); + + for (const auto& Element : ResponseJSON) + { + const TSharedPtr& ElementAsObject = Element->AsObject(); + OutResults.Add(FString::Format(TEXT("{0}\t{1}\t{2}"), {ElementAsObject->GetStringField("username"), + ElementAsObject->GetStringField("assetPath"), + ElementAsObject->GetStringField("projectName")})); + // deserialize here + } return true; } else { + OutErrorMessages.Add(FString::Format(TEXT("Request for file lock list resulted in HTTP error {0}"), + {Result.GetSubtype()})); return false; } } @@ -135,6 +174,8 @@ bool UModioLockProvider::LockFiles(const FString& InRepositoryRoot, const FGitFi } else { + OutErrorMessages.Add(FString::Format(TEXT("Request to lock file {0} resulted in HTTP error {1}"), + {*Params.FileNames[0], Result.GetSubtype()})); return false; } } @@ -151,6 +192,8 @@ bool UModioLockProvider::UnlockFiles(const FString& InRepositoryRoot, const FGit } else { + OutErrorMessages.Add(FString::Format(TEXT("Request to unlock file {0} resulted in HTTP error {1}"), + {*Params.FileNames[0], Result.GetSubtype()})); return false; } } diff --git a/Source/GitSourceControl/Private/ModioLockProvider.h b/Source/GitSourceControl/Private/ModioLockProvider.h index 0b0a97cb..0ad1af08 100644 --- a/Source/GitSourceControl/Private/ModioLockProvider.h +++ b/Source/GitSourceControl/Private/ModioLockProvider.h @@ -10,10 +10,6 @@ #include "ModioLockProvider.generated.h" -namespace EHttpRequestStatus -{ - enum Type; -} /** * */ @@ -29,8 +25,10 @@ class UModioLockProvider : public UObject, public IGitLockProvider const FString& FilePath, const FString& ProjectName); - TUnion PerformHttpRequest( - TSharedRef Request); + TUnion PerformHttpRequest(TSharedRef Request); + + TSharedPtr GetResponseAsJsonObject(const FString& ResponseString); + TArray> GetResponseAsJsonArray(const FString& ResponseString); void YieldThread(); From f8074488144088ad5f9b46667814a1daf3417d16 Mon Sep 17 00:00:00 2001 From: Stephen Whittle Date: Tue, 11 Feb 2025 13:31:11 +1100 Subject: [PATCH 06/11] Add new extended settings object for lock providers --- .../Private/GitSourceControlModule.cpp | 2 ++ .../Private/GitSourceControlSettings.cpp | 23 +++++++++++++++++++ .../Private/ModioLockProvider.cpp | 22 ++++++++++++++++++ .../Private/ModioLockProvider.h | 5 +++- .../Public/GitLockProviderSettings.h | 21 +++++++++++++++++ .../Public/GitSourceControlSettings.h | 5 ++++ .../Public/IGitLockProvider.h | 8 +++++++ 7 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 Source/GitSourceControl/Public/GitLockProviderSettings.h diff --git a/Source/GitSourceControl/Private/GitSourceControlModule.cpp b/Source/GitSourceControl/Private/GitSourceControlModule.cpp index 4f1aab3e..026cce45 100644 --- a/Source/GitSourceControl/Private/GitSourceControlModule.cpp +++ b/Source/GitSourceControl/Private/GitSourceControlModule.cpp @@ -163,6 +163,8 @@ void FGitSourceControlModule::UpdateLockProviderInstance() } LockProvider.Reset(NewObject(GetTransientPackage(), LockProviderClass)); + TArray Errors; + LockProvider->ConfigureWithSettings(GitSourceControlSettings.GetLockProviderSettings(), Errors); } void FGitSourceControlModule::ShutdownModule() diff --git a/Source/GitSourceControl/Private/GitSourceControlSettings.cpp b/Source/GitSourceControl/Private/GitSourceControlSettings.cpp index a5680795..6a5fb84d 100644 --- a/Source/GitSourceControl/Private/GitSourceControlSettings.cpp +++ b/Source/GitSourceControl/Private/GitSourceControlSettings.cpp @@ -13,6 +13,7 @@ namespace GitSettingsConstants /** The section of the ini file we load our settings from */ static const FString SettingsSection = TEXT("GitSourceControl.GitSourceControlSettings"); + static const FString LockProviderSettingsSection = TEXT("GitSourceControl.LockProviderSettings"); } // namespace GitSettingsConstants @@ -71,6 +72,11 @@ const TSoftClassPtr FGitSourceControlSettings::GetLo return LockProviderClass; } +const FGitLockProviderSettings& FGitSourceControlSettings::GetLockProviderSettings() const +{ + return CurrentLockProviderSettings; +} + bool FGitSourceControlSettings::SetLockProviderClass(TSoftClassPtr Provider) { LockProviderClass = Provider; @@ -93,6 +99,15 @@ void FGitSourceControlSettings::LoadSettings() LockProviderClassPath = TEXT("/Game/Blah/DefaultLockProvider"); } LockProviderClass = TSoftClassPtr {LockProviderClassPath}; + FConfigSection* LockProviderSettings = + GConfig->GetSectionPrivate(*GitSettingsConstants::LockProviderSettingsSection, false, true, IniFile); + if (LockProviderSettings) + { + for (const auto& Value : *LockProviderSettings) + { + CurrentLockProviderSettings.SettingValues.Add(Value.Key.ToString(), Value.Value.GetValue()); + } + } } void FGitSourceControlSettings::SaveSettings() const @@ -104,4 +119,12 @@ void FGitSourceControlSettings::SaveSettings() const GConfig->SetString(*GitSettingsConstants::SettingsSection, TEXT("LfsUserName"), *LfsUserName, IniFile); GConfig->SetString(*GitSettingsConstants::SettingsSection, TEXT("LockProviderClass"), *LockProviderClass.ToString(), IniFile); + FConfigSection* LockProviderSettings = + GConfig->GetSectionPrivate(*GitSettingsConstants::LockProviderSettingsSection, true, false, IniFile); + { + for (const auto& Value : CurrentLockProviderSettings.SettingValues) + { + LockProviderSettings->Add(FName(Value.Key), Value.Value); + } + } } diff --git a/Source/GitSourceControl/Private/ModioLockProvider.cpp b/Source/GitSourceControl/Private/ModioLockProvider.cpp index c25770cd..01ef4d1d 100644 --- a/Source/GitSourceControl/Private/ModioLockProvider.cpp +++ b/Source/GitSourceControl/Private/ModioLockProvider.cpp @@ -197,3 +197,25 @@ bool UModioLockProvider::UnlockFiles(const FString& InRepositoryRoot, const FGit return false; } } + +bool UModioLockProvider::ConfigureWithSettings(const FGitLockProviderSettings& NewSettings, TArray& OutErrors) +{ + if (NewSettings.SettingValues.Contains("ServerAddress")) + { + if (NewSettings.SettingValues.Contains("ServerPort")) + { + ServerAddress = FString::Format(TEXT("https://{0}:{1}"), {NewSettings.SettingValues["ServerAddress"], + NewSettings.SettingValues["ServerPort"]}); + return true; + } + else + { + OutErrors.Add("ServerPort setting missing"); + } + } + else + { + OutErrors.Add("ServerAddress setting missing"); + } + return false; +} diff --git a/Source/GitSourceControl/Private/ModioLockProvider.h b/Source/GitSourceControl/Private/ModioLockProvider.h index 0ad1af08..d1088be9 100644 --- a/Source/GitSourceControl/Private/ModioLockProvider.h +++ b/Source/GitSourceControl/Private/ModioLockProvider.h @@ -14,7 +14,7 @@ * */ UCLASS() -class UModioLockProvider : public UObject, public IGitLockProvider +class UModioLockProvider : public UGitLockProviderBase { GENERATED_BODY() TSharedRef GetLocksRequest(); @@ -31,6 +31,7 @@ class UModioLockProvider : public UObject, public IGitLockProvider TArray> GetResponseAsJsonArray(const FString& ResponseString); void YieldThread(); + FString ServerAddress; public: bool RunLFSCommand(const FString& InCommand, const FString& InRepositoryRoot, const FString& GitBinaryFallback, @@ -45,4 +46,6 @@ class UModioLockProvider : public UObject, public IGitLockProvider bool UnlockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, TArray& OutResults, TArray& OutErrorMessages) override; + + bool ConfigureWithSettings(const FGitLockProviderSettings& NewSettings, TArray& OutErrors) override; }; diff --git a/Source/GitSourceControl/Public/GitLockProviderSettings.h b/Source/GitSourceControl/Public/GitLockProviderSettings.h new file mode 100644 index 00000000..ef2a1314 --- /dev/null +++ b/Source/GitSourceControl/Public/GitLockProviderSettings.h @@ -0,0 +1,21 @@ +// 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/Map.h" +#include "Containers/UnrealString.h" + +#include "GitLockProviderSettings.generated.h" + + +USTRUCT() +struct FGitLockProviderSettings +{ + GENERATED_BODY() + + UPROPERTY() + TMap SettingValues; +}; diff --git a/Source/GitSourceControl/Public/GitSourceControlSettings.h b/Source/GitSourceControl/Public/GitSourceControlSettings.h index b5cab2ff..e7719ae5 100644 --- a/Source/GitSourceControl/Public/GitSourceControlSettings.h +++ b/Source/GitSourceControl/Public/GitSourceControlSettings.h @@ -6,6 +6,7 @@ #pragma once #include "Containers/UnrealString.h" +#include "GitLockProviderSettings.h" #include "HAL/CriticalSection.h" class GITSOURCECONTROL_API FGitSourceControlSettings @@ -31,6 +32,8 @@ class GITSOURCECONTROL_API FGitSourceControlSettings const TSoftClassPtr GetLockProviderClass() const; + const FGitLockProviderSettings& GetLockProviderSettings() const; + /** Load settings from ini file */ void LoadSettings(); @@ -54,4 +57,6 @@ class GITSOURCECONTROL_API FGitSourceControlSettings FString LfsUserName; TSoftClassPtr LockProviderClass; + + FGitLockProviderSettings CurrentLockProviderSettings; }; diff --git a/Source/GitSourceControl/Public/IGitLockProvider.h b/Source/GitSourceControl/Public/IGitLockProvider.h index 935c852b..b6186a72 100644 --- a/Source/GitSourceControl/Public/IGitLockProvider.h +++ b/Source/GitSourceControl/Public/IGitLockProvider.h @@ -1,5 +1,7 @@ #pragma once +#include "GitLockProviderSettings.h" + #include "IGitLockProvider.generated.h" UINTERFACE(meta = (CannotImplementInterfaceInBlueprint)) @@ -20,6 +22,7 @@ class IGitLockProvider { GENERATED_BODY() public: + virtual bool ConfigureWithSettings(const FGitLockProviderSettings& NewSettings, TArray& OutErrors) = 0; virtual bool RunLFSCommand(const FString& InCommand, const FString& InRepositoryRoot, const FString& GitBinaryFallback, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, @@ -61,4 +64,9 @@ class UGitLockProviderBase : public UObject, public IGitLockProvider { return false; } + + bool ConfigureWithSettings(const FGitLockProviderSettings& NewSettings, TArray& OutErrors) override + { + return true; + } }; From 48c9944c5db5043184dd45be4cbfcb112fe4426e Mon Sep 17 00:00:00 2001 From: Stephen Whittle Date: Wed, 12 Feb 2025 12:46:49 +1100 Subject: [PATCH 07/11] Settings validation now correctly prints to the right log --- .../Private/GitSourceControlModule.cpp | 9 ++- .../Private/SGitSourceControlSettings.cpp | 65 +++++++++++++++++-- .../Private/SGitSourceControlSettings.h | 9 ++- .../Public/GitLockProviderSettings.h | 3 +- 4 files changed, 76 insertions(+), 10 deletions(-) diff --git a/Source/GitSourceControl/Private/GitSourceControlModule.cpp b/Source/GitSourceControl/Private/GitSourceControlModule.cpp index 026cce45..89d930b1 100644 --- a/Source/GitSourceControl/Private/GitSourceControlModule.cpp +++ b/Source/GitSourceControl/Private/GitSourceControlModule.cpp @@ -25,6 +25,7 @@ #include "GitSourceControlUtils.h" #include "ISourceControlModule.h" #include "LFSLockProvider.h" +#include "Logging/MessageLog.h" #include "Misc/ConfigCacheIni.h" #include "SourceControlHelpers.h" @@ -159,12 +160,18 @@ void FGitSourceControlModule::UpdateLockProviderInstance() TSoftClassPtr LockProviderClassPtr = GitSourceControlSettings.GetLockProviderClass(); if (LockProviderClassPtr.IsValid()) { - LockProviderClassPtr.LoadSynchronous(); + LockProviderClass = LockProviderClassPtr.LoadSynchronous(); } LockProvider.Reset(NewObject(GetTransientPackage(), LockProviderClass)); TArray Errors; LockProvider->ConfigureWithSettings(GitSourceControlSettings.GetLockProviderSettings(), Errors); + for (const FString& CurrentError : Errors) + { + FMessageLog("SourceControl") + .Error(FText::FromString( + FString::Format(TEXT("Lock Provider Settings validation failure:{0}"), {*CurrentError}))); + } } void FGitSourceControlModule::ShutdownModule() diff --git a/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp b/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp index 0c9428d6..4bd42384 100644 --- a/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp +++ b/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp @@ -8,10 +8,19 @@ #include "EditorDirectories.h" #include "Fonts/SlateFontInfo.h" #include "Framework/Notifications/NotificationManager.h" +#include "GitLockProviderSettings.h" +#include "GitSourceControlModule.h" +#include "GitSourceControlUtils.h" +#include "IGitLockProvider.h" +#include "ISourceControlModule.h" +#include "IStructureDetailsView.h" +#include "LFSLockProvider.h" +#include "Logging/MessageLog.h" #include "Misc/App.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "Modules/ModuleManager.h" +#include "PropertyCustomizationHelpers.h" #include "Runtime/Launch/Resources/Version.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SEditableTextBox.h" @@ -25,15 +34,56 @@ #else #include "EditorStyleSet.h" #endif -#include "GitSourceControlModule.h" -#include "GitSourceControlUtils.h" -#include "IGitLockProvider.h" -#include "LFSLockProvider.h" -#include "PropertyCustomizationHelpers.h" #include "SourceControlOperations.h" #define LOCTEXT_NAMESPACE "SGitSourceControlSettings" +TSharedRef SGitSourceControlSettings::ConstructLockProviderSettingsWidget() +{ + FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); + FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); + + LockProviderSettings = + MakeShared(FGitLockProviderSettings::StaticStruct(), + (uint8*) &GitSourceControl.AccessSettings().GetLockProviderSettings()) + .ToSharedPtr(); + FDetailsViewArgs DetailArgs; + DetailArgs.bUpdatesFromSelection = false; + DetailArgs.bLockable = false; + DetailArgs.NameAreaSettings = FDetailsViewArgs::ComponentsAndActorsUseNameArea; + DetailArgs.bCustomNameAreaLocation = false; + DetailArgs.bCustomFilterAreaLocation = false; + DetailArgs.bShowOptions = false; + DetailArgs.bAllowSearch = false; + DetailArgs.DefaultsOnlyVisibility = EEditDefaultsOnlyNodeVisibility::Show; + DetailArgs.bForceHiddenPropertyVisibility = true; + + DetailArgs.NotifyHook = this; + + return PropertyModule + .CreateStructureDetailView(DetailArgs, {}, LockProviderSettings, FText::FromString("Lock Provider Settings")) + ->GetWidget() + .ToSharedRef(); +} + +void SGitSourceControlSettings::NotifyPostChange(const FPropertyChangedEvent& PropertyChangedEvent, + FProperty* PropertyThatChanged) +{ + if (PropertyChangedEvent.GetPropertyName() == FName("SettingValues")) + { + FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); + TArray OutErrors; + GitSourceControl.GetLockProvider()->ConfigureWithSettings( + GitSourceControl.AccessSettings().GetLockProviderSettings(), OutErrors); + for (const FString& CurrentError : OutErrors) + { + FMessageLog("SourceControl") + .Error(FText::FromString( + FString::Format(TEXT("Lock Provider Settings validation failure: {0}"), {*CurrentError}))); + } + } +} + void SGitSourceControlSettings::Construct(const FArguments& InArgs) { bAutoCreateGitIgnore = true; @@ -557,6 +607,11 @@ void SGitSourceControlSettings::ConstructBasedOnEngineVersion( ) .SelectedClass(this, &Self::GetLockProviderClass) .OnSetClass(this, &Self::SetLockProviderClass) ] + ] + + SVerticalBox::Slot() + .AutoHeight() + [ + ConstructLockProviderSettingsWidget() ] // [Optional] Initial Git Commit +SVerticalBox::Slot() diff --git a/Source/GitSourceControl/Private/SGitSourceControlSettings.h b/Source/GitSourceControl/Private/SGitSourceControlSettings.h index 16365643..adcd39c2 100644 --- a/Source/GitSourceControl/Private/SGitSourceControlSettings.h +++ b/Source/GitSourceControl/Private/SGitSourceControlSettings.h @@ -6,7 +6,9 @@ #pragma once #include "ISourceControlProvider.h" +#include "Misc/NotifyHook.h" #include "Runtime/Launch/Resources/Version.h" +#include "UObject/StructOnScope.h" #include "Widgets/SCompoundWidget.h" class SNotificationItem; @@ -24,13 +26,15 @@ namespace ETextCommit enum class ECheckBoxState : uint8; -class SGitSourceControlSettings : public SCompoundWidget +class SGitSourceControlSettings : public SCompoundWidget, public FNotifyHook { public: SLATE_BEGIN_ARGS(SGitSourceControlSettings) {} SLATE_END_ARGS() + void NotifyPostChange(const FPropertyChangedEvent& PropertyChangedEvent, FProperty* PropertyThatChanged) override; + public: void Construct(const FArguments& InArgs); @@ -99,7 +103,8 @@ class SGitSourceControlSettings : public SCompoundWidget /** Asynchronous operation progress notifications */ TWeakPtr OperationInProgressNotification; - + TSharedRef ConstructLockProviderSettingsWidget(); + TSharedPtr LockProviderSettings; void DisplayInProgressNotification(const FSourceControlOperationRef& InOperation); void RemoveInProgressNotification(); void DisplaySuccessNotification(const FSourceControlOperationRef& InOperation); diff --git a/Source/GitSourceControl/Public/GitLockProviderSettings.h b/Source/GitSourceControl/Public/GitLockProviderSettings.h index ef2a1314..f1899e52 100644 --- a/Source/GitSourceControl/Public/GitLockProviderSettings.h +++ b/Source/GitSourceControl/Public/GitLockProviderSettings.h @@ -10,12 +10,11 @@ #include "GitLockProviderSettings.generated.h" - USTRUCT() struct FGitLockProviderSettings { GENERATED_BODY() - UPROPERTY() + UPROPERTY(EditAnywhere) TMap SettingValues; }; From 1595295a547258f2542f143ae45c053cab0e5dae Mon Sep 17 00:00:00 2001 From: Stephen Whittle Date: Wed, 12 Feb 2025 14:18:32 +1100 Subject: [PATCH 08/11] Solve a thread-safety issue caused by the optimizer not treating a bool as volatile; correctly deserialize lock objects from the HTTP API --- .../Private/GitSourceControlSettings.cpp | 3 +- .../Private/ModioLockProvider.cpp | 39 +++++++++++-------- .../Private/SGitSourceControlSettings.cpp | 4 ++ 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/Source/GitSourceControl/Private/GitSourceControlSettings.cpp b/Source/GitSourceControl/Private/GitSourceControlSettings.cpp index 6a5fb84d..154ab24c 100644 --- a/Source/GitSourceControl/Private/GitSourceControlSettings.cpp +++ b/Source/GitSourceControl/Private/GitSourceControlSettings.cpp @@ -96,7 +96,7 @@ void FGitSourceControlSettings::LoadSettings() IniFile); if (LockProviderClassPath.IsEmpty()) { - LockProviderClassPath = TEXT("/Game/Blah/DefaultLockProvider"); + LockProviderClassPath = TEXT("/Script/GitSourceControl.LFSLockProvider"); } LockProviderClass = TSoftClassPtr {LockProviderClassPath}; FConfigSection* LockProviderSettings = @@ -127,4 +127,5 @@ void FGitSourceControlSettings::SaveSettings() const LockProviderSettings->Add(FName(Value.Key), Value.Value); } } + GConfig->Flush(false, IniFile); } diff --git a/Source/GitSourceControl/Private/ModioLockProvider.cpp b/Source/GitSourceControl/Private/ModioLockProvider.cpp index 01ef4d1d..92299011 100644 --- a/Source/GitSourceControl/Private/ModioLockProvider.cpp +++ b/Source/GitSourceControl/Private/ModioLockProvider.cpp @@ -21,7 +21,7 @@ TSharedRef UModioLockProvider::GetLocksReques FHttpModule& HttpModule = FHttpModule::Get(); TSharedRef Request = HttpModule.CreateRequest(); - FString RequestURL = FString("ServerIP") + TEXT("/api/FileLock/lock"); + FString RequestURL = ServerAddress + TEXT("/api/FileLock/lock"); Request->SetVerb(TEXT("GET")); Request->SetURL(RequestURL); return Request; @@ -34,7 +34,7 @@ TSharedRef UModioLockProvider::LockFile FHttpModule& HttpModule = FHttpModule::Get(); TSharedRef Request = HttpModule.CreateRequest(); - FString RequestURL = FString("ServerIP") + TEXT("/api/FileLock/lock"); + FString RequestURL = ServerAddress + TEXT("/api/FileLock/lock"); Request->SetVerb(TEXT("POST")); Request->SetURL(RequestURL); Request->SetHeader(TEXT("Content-Type"), TEXT("application/json")); @@ -52,7 +52,7 @@ TSharedRef UModioLockProvider::UnlockFi FHttpModule& HttpModule = FHttpModule::Get(); TSharedRef Request = HttpModule.CreateRequest(); - FString RequestURL = FString("ServerIP") + TEXT("/api/FileLock/lock"); + FString RequestURL = ServerAddress + TEXT("/api/FileLock/lock"); Request->SetVerb(TEXT("DELETE")); Request->SetURL(RequestURL); Request->SetHeader(TEXT("Content-Type"), TEXT("application/json")); @@ -66,10 +66,11 @@ TSharedRef UModioLockProvider::UnlockFi TUnion UModioLockProvider::PerformHttpRequest(TSharedRef Request) { TUnion Result; - bool bRequestDone = false; + Result.SetSubtype(-1); + volatile bool bRequestDone = false; Request->OnProcessRequestComplete().BindLambda( [&](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bConnectedSuccessfully) { - if (bConnectedSuccessfully) + if (Response->GetResponseCode() == 200) { Result.SetSubtype(Response->GetContentAsString()); } @@ -77,6 +78,7 @@ TUnion UModioLockProvider::PerformHttpRequest(TSharedRef(Response->GetResponseCode()); } + bRequestDone = true; }); Request->ProcessRequest(); while (!bRequestDone) @@ -116,15 +118,18 @@ TArray> UModioLockProvider::GetResponseAsJsonArray(const void UModioLockProvider::YieldThread() { - FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread); + /*if (FTaskGraphInterface::Get().GetCurrentThread() == ENamedThreads::GameThread) + { + FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread); #if UE_VERSION_OLDER_THAN(5, 3, 0) - FTicker::GetCoreTicker().Tick(FApp::GetDeltaTime()); + FTicker::GetCoreTicker().Tick(FApp::GetDeltaTime()); #else - FTSTicker::GetCoreTicker().Tick(FApp::GetDeltaTime()); + FTSTicker::GetCoreTicker().Tick(FApp::GetDeltaTime()); #endif - FSlateApplication::Get().PumpMessages(); - FSlateApplication::Get().Tick(); - FPlatformProcess::Sleep(0); + FSlateApplication::Get().PumpMessages(); + FSlateApplication::Get().Tick(); + FPlatformProcess::Sleep(0); + }*/ } bool UModioLockProvider::RunLFSCommand(const FString& InCommand, const FString& InRepositoryRoot, @@ -147,9 +152,9 @@ bool UModioLockProvider::GetLockedFiles(const FString& InRepositoryRoot, const F for (const auto& Element : ResponseJSON) { const TSharedPtr& ElementAsObject = Element->AsObject(); - OutResults.Add(FString::Format(TEXT("{0}\t{1}\t{2}"), {ElementAsObject->GetStringField("username"), - ElementAsObject->GetStringField("assetPath"), - ElementAsObject->GetStringField("projectName")})); + OutResults.Add(FString::Format(TEXT("{0}\t{1}\t{2}"), {ElementAsObject->GetStringField("assetPath"), + ElementAsObject->GetStringField("username"), + ElementAsObject->GetStringField("id")})); // deserialize here } return true; @@ -204,8 +209,10 @@ bool UModioLockProvider::ConfigureWithSettings(const FGitLockProviderSettings& N { if (NewSettings.SettingValues.Contains("ServerPort")) { - ServerAddress = FString::Format(TEXT("https://{0}:{1}"), {NewSettings.SettingValues["ServerAddress"], - NewSettings.SettingValues["ServerPort"]}); + ServerAddress = + FString::Format(TEXT("https://{0}:{1}"), {NewSettings.SettingValues["ServerAddress"].TrimStartAndEnd(), + NewSettings.SettingValues["ServerPort"].TrimStartAndEnd()}) + .TrimStartAndEnd(); return true; } else diff --git a/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp b/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp index 4bd42384..80e14825 100644 --- a/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp +++ b/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp @@ -81,6 +81,10 @@ void SGitSourceControlSettings::NotifyPostChange(const FPropertyChangedEvent& Pr .Error(FText::FromString( FString::Format(TEXT("Lock Provider Settings validation failure: {0}"), {*CurrentError}))); } + if (!OutErrors.Num()) + { + GitSourceControl.SaveSettings(); + } } } From 541105a5bff7c4a372fe03a608373e3457e66119 Mon Sep 17 00:00:00 2001 From: Stephen Whittle Date: Wed, 19 Feb 2025 12:17:34 +1100 Subject: [PATCH 09/11] Lock providers now handle which extensions are lockable --- .../Private/GitSourceControlUtils.cpp | 25 ++---------- .../Private/LFSLockProvider.cpp | 28 +++++++++++++ .../Private/LFSLockProvider.h | 4 ++ .../Private/ModioLockProvider.cpp | 39 ++++++++++++++++--- .../Private/ModioLockProvider.h | 4 ++ .../Public/IGitLockProvider.h | 10 +++++ 6 files changed, 82 insertions(+), 28 deletions(-) diff --git a/Source/GitSourceControl/Private/GitSourceControlUtils.cpp b/Source/GitSourceControl/Private/GitSourceControlUtils.cpp index efdd81c3..455b76d8 100644 --- a/Source/GitSourceControl/Private/GitSourceControlUtils.cpp +++ b/Source/GitSourceControl/Private/GitSourceControlUtils.cpp @@ -2461,28 +2461,9 @@ namespace GitSourceControlUtils bool CheckLFSLockable(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InFiles, TArray& OutErrorMessages) { - TArray Results; - TArray Parameters; - Parameters.Add(TEXT("lockable")); // follow file renames - - const bool bResults = RunCommand(TEXT("check-attr"), InPathToGitBinary, InRepositoryRoot, Parameters, InFiles, - Results, OutErrorMessages); - if (!bResults) - { - return false; - } - - for (int i = 0; i < InFiles.Num(); i++) - { - const FString& Result = Results[i]; - if (Result.EndsWith("set") && !Result.EndsWith("unset")) - { - const FString FileExt = InFiles[i].RightChop(1); // Remove wildcard (*) - LockableTypes.Add(FileExt); - } - } - - return true; + FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); + return GitSourceControl.GetLockProvider()->CheckLockableExtensions(InPathToGitBinary, InRepositoryRoot, InFiles, + LockableTypes, OutErrorMessages); } bool FetchRemote(const FString& InPathToGitBinary, const FString& InPathToRepositoryRoot, bool InUsingGitLfsLocking, diff --git a/Source/GitSourceControl/Private/LFSLockProvider.cpp b/Source/GitSourceControl/Private/LFSLockProvider.cpp index b9ec228b..8aafcbc7 100644 --- a/Source/GitSourceControl/Private/LFSLockProvider.cpp +++ b/Source/GitSourceControl/Private/LFSLockProvider.cpp @@ -57,3 +57,31 @@ bool ULFSLockProvider::UnlockFiles(const FString& InRepositoryRoot, const FGitFi return RunLFSCommand(TEXT("unlock"), InRepositoryRoot, Params.GitBinaryPath, Params.CustomParams, Params.FileNames, OutResults, OutErrorMessages); } + +bool ULFSLockProvider::CheckLockableExtensions(const FString& InPathToGitBinary, const FString& InRepositoryRoot, + const TArray& InFiles, TArray& OutLockableExtensions, + TArray& OutErrorMessages) +{ + TArray Results; + TArray Parameters; + Parameters.Add(TEXT("lockable")); // follow file renames + + const bool bResults = GitSourceControlUtils::RunCommand(TEXT("check-attr"), InPathToGitBinary, InRepositoryRoot, + Parameters, InFiles, Results, OutErrorMessages); + if (!bResults) + { + return false; + } + + for (int i = 0; i < InFiles.Num(); i++) + { + const FString& Result = Results[i]; + if (Result.EndsWith("set") && !Result.EndsWith("unset")) + { + const FString FileExt = InFiles[i].RightChop(1); // Remove wildcard (*) + OutLockableExtensions.Add(FileExt); + } + } + + return true; +} diff --git a/Source/GitSourceControl/Private/LFSLockProvider.h b/Source/GitSourceControl/Private/LFSLockProvider.h index 5fcca02f..fbce131c 100644 --- a/Source/GitSourceControl/Private/LFSLockProvider.h +++ b/Source/GitSourceControl/Private/LFSLockProvider.h @@ -22,4 +22,8 @@ class ULFSLockProvider : public UGitLockProviderBase virtual bool UnlockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, TArray& OutResults, TArray& OutErrorMessages) override; + + bool CheckLockableExtensions(const FString& InPathToGitBinary, const FString& InRepositoryRoot, + const TArray& InFiles, TArray& OutLockableExtensions, + TArray& OutErrorMessages) override; }; diff --git a/Source/GitSourceControl/Private/ModioLockProvider.cpp b/Source/GitSourceControl/Private/ModioLockProvider.cpp index 92299011..7b8569af 100644 --- a/Source/GitSourceControl/Private/ModioLockProvider.cpp +++ b/Source/GitSourceControl/Private/ModioLockProvider.cpp @@ -4,8 +4,11 @@ #include "Async/TaskGraphInterfaces.h" #include "Containers/Ticker.h" #include "Framework/Application/SlateApplication.h" +#include "GenericPlatform/GenericPlatformHttp.h" +#include "GitMessageLog.h" #include "GitSourceControlModule.h" #include "HAL/PlatformProcess.h" +#include "HttpManager.h" #include "HttpModule.h" #include "ISourceControlModule.h" #include "Interfaces/IHttpRequest.h" @@ -39,7 +42,7 @@ TSharedRef UModioLockProvider::LockFile Request->SetURL(RequestURL); Request->SetHeader(TEXT("Content-Type"), TEXT("application/json")); FString RequestContent = FString::Format(TEXT("{\"username\": \"{0}\", \"assetPath\": \"{1}\", \"projectName\": " - "\"{2}\", \"bCreateProject\": \"true\" }"), + "\"{2}\", \"bCreateProject\": true }"), {*Username, *FilePath, *ProjectName}); Request->SetContentAsString(RequestContent); return Request; @@ -67,9 +70,18 @@ TUnion UModioLockProvider::PerformHttpRequest(TSharedRef Result; Result.SetSubtype(-1); - volatile bool bRequestDone = false; Request->OnProcessRequestComplete().BindLambda( - [&](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bConnectedSuccessfully) { + [&Result](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bConnectedSuccessfully) { + // need to check validity of request and response here + if (!Request.IsValid() || !Response.IsValid()) + { + Result.SetSubtype(404); + return; + } + FTSMessageLog("SourceControl") + .Info(FText::FromString( + FString::Format(TEXT("Request {0} {1} received response code {2}"), + {Request->GetVerb(), *Request->GetURL(), Response->GetResponseCode()}))); if (Response->GetResponseCode() == 200) { Result.SetSubtype(Response->GetContentAsString()); @@ -78,13 +90,14 @@ TUnion UModioLockProvider::PerformHttpRequest(TSharedRef(Response->GetResponseCode()); } - bRequestDone = true; }); Request->ProcessRequest(); - while (!bRequestDone) + while (Request->GetStatus() == EHttpRequestStatus::Processing) { - YieldThread(); + FPlatformProcess::Sleep(0.01f); } + // synchronous event from task pool + return Result; } @@ -226,3 +239,17 @@ bool UModioLockProvider::ConfigureWithSettings(const FGitLockProviderSettings& N } return false; } + +bool UModioLockProvider::CheckLockableExtensions(const FString& InPathToGitBinary, const FString& InRepositoryRoot, + const TArray& InFiles, TArray& OutLockableExtensions, + TArray& OutErrorMessages) +{ + for (const FString& Extension : InFiles) + { + if (Extension.StartsWith(TEXT("*"))) + { + OutLockableExtensions.Add(Extension.Mid(1)); + } + } + return true; +} diff --git a/Source/GitSourceControl/Private/ModioLockProvider.h b/Source/GitSourceControl/Private/ModioLockProvider.h index d1088be9..58106460 100644 --- a/Source/GitSourceControl/Private/ModioLockProvider.h +++ b/Source/GitSourceControl/Private/ModioLockProvider.h @@ -48,4 +48,8 @@ class UModioLockProvider : public UGitLockProviderBase TArray& OutErrorMessages) override; bool ConfigureWithSettings(const FGitLockProviderSettings& NewSettings, TArray& OutErrors) override; + + bool CheckLockableExtensions(const FString& InPathToGitBinary, const FString& InRepositoryRoot, + const TArray& InFiles, TArray& OutLockableExtensions, + TArray& OutErrorMessages) override; }; diff --git a/Source/GitSourceControl/Public/IGitLockProvider.h b/Source/GitSourceControl/Public/IGitLockProvider.h index b6186a72..893b3c63 100644 --- a/Source/GitSourceControl/Public/IGitLockProvider.h +++ b/Source/GitSourceControl/Public/IGitLockProvider.h @@ -33,6 +33,9 @@ class IGitLockProvider TArray& OutResults, TArray& OutErrorMessages) = 0; virtual bool UnlockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, TArray& OutResults, TArray& OutErrorMessages) = 0; + virtual bool CheckLockableExtensions(const FString& InPathToGitBinary, const FString& InRepositoryRoot, + const TArray& InFiles, TArray& OutLockableExtensions, + TArray& OutErrorMessages) = 0; }; UCLASS(Abstract) @@ -69,4 +72,11 @@ class UGitLockProviderBase : public UObject, public IGitLockProvider { return true; } + + bool CheckLockableExtensions(const FString& InPathToGitBinary, const FString& InRepositoryRoot, + const TArray& InFiles, TArray& OutLockableExtensions, + TArray& OutErrorMessages) override + { + return true; + } }; From b3dab6056840ff109c127b5ac36c3197b8306d14 Mon Sep 17 00:00:00 2001 From: Stephen Whittle Date: Fri, 21 Feb 2025 15:34:20 +1100 Subject: [PATCH 10/11] This now works with the addition of the 'complete on http thread' policy --- .../Private/ModioLockProvider.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Source/GitSourceControl/Private/ModioLockProvider.cpp b/Source/GitSourceControl/Private/ModioLockProvider.cpp index 7b8569af..00a82e13 100644 --- a/Source/GitSourceControl/Private/ModioLockProvider.cpp +++ b/Source/GitSourceControl/Private/ModioLockProvider.cpp @@ -55,14 +55,11 @@ TSharedRef UModioLockProvider::UnlockFi FHttpModule& HttpModule = FHttpModule::Get(); TSharedRef Request = HttpModule.CreateRequest(); - FString RequestURL = ServerAddress + TEXT("/api/FileLock/lock"); + FString RequestURL = + ServerAddress + FString::Format(TEXT("/api/FileLock/lock?username={0}&assetPath={1}&projectName={2}"), + {*Username, *FGenericPlatformHttp::UrlEncode(FilePath), *ProjectName}); Request->SetVerb(TEXT("DELETE")); Request->SetURL(RequestURL); - Request->SetHeader(TEXT("Content-Type"), TEXT("application/json")); - FString RequestContent = FString::Format(TEXT("{\"username\": \"{0}\", \"assetPath\": \"{1}\", \"projectName\": " - "\"{2}\" }"), - {*Username, *FilePath, *ProjectName}); - Request->SetContentAsString(RequestContent); return Request; } @@ -70,6 +67,7 @@ TUnion UModioLockProvider::PerformHttpRequest(TSharedRef Result; Result.SetSubtype(-1); + Request->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); Request->OnProcessRequestComplete().BindLambda( [&Result](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bConnectedSuccessfully) { // need to check validity of request and response here @@ -92,9 +90,13 @@ TUnion UModioLockProvider::PerformHttpRequest(TSharedRefProcessRequest(); + double LastTime = FPlatformTime::Seconds(); while (Request->GetStatus() == EHttpRequestStatus::Processing) { - FPlatformProcess::Sleep(0.01f); + // const double AppTime = FPlatformTime::Seconds(); + // FHttpModule::Get().GetHttpManager().Tick(AppTime - LastTime); + // LastTime = AppTime; + FPlatformProcess::Sleep(0.1f); } // synchronous event from task pool From 6b72b52f525d5d40198517b7431ef27a82f3ac13 Mon Sep 17 00:00:00 2001 From: Kristopher Karadimas Date: Wed, 16 Sep 2026 11:06:58 +1000 Subject: [PATCH 11/11] added support for batch locking & unlocking --- .../Private/GitSourceControlOperations.cpp | 78 +++-- .../Private/LFSLockProvider.cpp | 25 +- .../Private/LFSLockProvider.h | 6 +- .../Private/ModioLockProvider.cpp | 293 ++++++++++++++---- .../Private/ModioLockProvider.h | 26 +- .../Public/IGitLockProvider.h | 17 +- 6 files changed, 341 insertions(+), 104 deletions(-) diff --git a/Source/GitSourceControl/Private/GitSourceControlOperations.cpp b/Source/GitSourceControl/Private/GitSourceControlOperations.cpp index 6d5dc946..b740d57a 100644 --- a/Source/GitSourceControl/Private/GitSourceControlOperations.cpp +++ b/Source/GitSourceControl/Private/GitSourceControlOperations.cpp @@ -28,6 +28,30 @@ #define LOCTEXT_NAMESPACE "GitSourceControl" +namespace GitSourceControlOperationsHelpers +{ + //Drops the cached lock entries for the files a provider actually released. + static void ForgetReleasedLocks(const FString& InPathToGitRoot, const TArray& InLockedAbsoluteFiles, + const TArray& InReleasedRelativeFiles) + { + if (InReleasedRelativeFiles.Num() == 0) + { + return; + } + + const TSet ReleasedFiles(InReleasedRelativeFiles); + for (const FString& AbsoluteFile : InLockedAbsoluteFiles) + { + const TArray RelativeFile = + GitSourceControlUtils::RelativeFilenames({AbsoluteFile}, InPathToGitRoot); + if (RelativeFile.Num() == 1 && ReleasedFiles.Contains(RelativeFile[0])) + { + FGitLockedFilesCache::RemoveLockedFile(AbsoluteFile); + } + } + } +} // namespace GitSourceControlOperationsHelpers + FName FGitConnectWorker::GetName() const { return "Connect"; @@ -130,24 +154,30 @@ bool FGitCheckOutWorker::Execute(FGitSourceControlCommand& InCommand) InCommand.bCommandSuccessful = true; return InCommand.bCommandSuccessful; } + // Locking can be a partial-success, some files can come back locked while others are held by someone + // else, so the provider reports which ones we actually hold. + TArray LockedRelativeFiles; const bool bSuccess = FGitSourceControlModule::Get().GetLockProvider()->LockFiles( InCommand.PathToGitRoot, FGitFileLockOpParams {InCommand.PathToGitBinary, FGitSourceControlModule::GetEmptyStringArray(), false, LockableRelativeFiles}, - InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); + LockedRelativeFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); InCommand.bCommandSuccessful = bSuccess; const FString& LockUser = FGitSourceControlModule::Get().GetProvider().GetLockUser(); - if (bSuccess) + + // Mark exactly the files the provider locked, not the whole selection + TArray AbsoluteFiles; + AbsoluteFiles.Reserve(LockedRelativeFiles.Num()); + for (const auto& RelativeFile : LockedRelativeFiles) { - TArray AbsoluteFiles; - for (const auto& RelativeFile : RelativeFiles) - { - FString AbsoluteFile = FPaths::Combine(InCommand.PathToGitRoot, RelativeFile); - FGitLockedFilesCache::AddLockedFile(AbsoluteFile, LockUser); - FPaths::NormalizeFilename(AbsoluteFile); - AbsoluteFiles.Add(AbsoluteFile); - } + FString AbsoluteFile = FPaths::Combine(InCommand.PathToGitRoot, RelativeFile); + FGitLockedFilesCache::AddLockedFile(AbsoluteFile, LockUser); + FPaths::NormalizeFilename(AbsoluteFile); + AbsoluteFiles.Add(AbsoluteFile); + } + if (AbsoluteFiles.Num() > 0) + { GitSourceControlUtils::CollectNewStates(AbsoluteFiles, States, EFileState::Unset, ETreeState::Unset, ELockState::Locked); for (auto& State : States) @@ -373,18 +403,15 @@ bool FGitCheckInWorker::Execute(FGitSourceControlCommand& InCommand) if (FilesToUnlock.Num() > 0) { // Not strictly necessary to succeed, so don't update command success - const bool bUnlockSuccess = FGitSourceControlModule::Get().GetLockProvider()->UnlockFiles( + TArray UnlockedFiles; + FGitSourceControlModule::Get().GetLockProvider()->UnlockFiles( InCommand.PathToGitRoot, FGitFileLockOpParams {InCommand.PathToGitBinary, FGitSourceControlModule::GetEmptyStringArray(), false, FilesToUnlock}, - InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); - if (bUnlockSuccess) - { - for (const auto& File : LockedFiles) - { - FGitLockedFilesCache::RemoveLockedFile(File); - } - } + UnlockedFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); + // Unlocking is partial-success too, so forget only the files actually released. + GitSourceControlOperationsHelpers::ForgetReleasedLocks(InCommand.PathToGitRoot, LockedFiles, + UnlockedFiles); } } #if 0 @@ -647,18 +674,15 @@ bool FGitRevertWorker::Execute(FGitSourceControlCommand& InCommand) { const TArray& RelativeFiles = GitSourceControlUtils::RelativeFilenames(LockedFiles, InCommand.PathToGitRoot); + TArray UnlockedFiles; InCommand.bCommandSuccessful &= FGitSourceControlModule::Get().GetLockProvider()->UnlockFiles( InCommand.PathToGitRoot, FGitFileLockOpParams {InCommand.PathToGitBinary, FGitSourceControlModule::GetEmptyStringArray(), false, RelativeFiles}, - InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); - if (InCommand.bCommandSuccessful) - { - for (const auto& File : LockedFiles) - { - FGitLockedFilesCache::RemoveLockedFile(File); - } - } + UnlockedFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); + // Forget only the files actually released, so a lock we failed to drop stays visible. + GitSourceControlOperationsHelpers::ForgetReleasedLocks(InCommand.PathToGitRoot, LockedFiles, + UnlockedFiles); } } diff --git a/Source/GitSourceControl/Private/LFSLockProvider.cpp b/Source/GitSourceControl/Private/LFSLockProvider.cpp index 8aafcbc7..d04bab16 100644 --- a/Source/GitSourceControl/Private/LFSLockProvider.cpp +++ b/Source/GitSourceControl/Private/LFSLockProvider.cpp @@ -45,17 +45,30 @@ bool ULFSLockProvider::GetLockedFiles(const FString& InRepositoryRoot, const FGi } bool ULFSLockProvider::LockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, - TArray& OutResults, TArray& OutErrorMessages) + TArray& OutSucceededFiles, TArray& OutResults, + TArray& OutErrorMessages) { - return RunLFSCommand(TEXT("lock"), InRepositoryRoot, Params.GitBinaryPath, Params.CustomParams, Params.FileNames, - OutResults, OutErrorMessages); + const bool bSuccess = RunLFSCommand(TEXT("lock"), InRepositoryRoot, Params.GitBinaryPath, Params.CustomParams, + Params.FileNames, OutResults, OutErrorMessages); + // git-lfs locks the whole invocation or none of it, so there is no partial outcome to report. + if (bSuccess) + { + OutSucceededFiles.Append(Params.FileNames); + } + return bSuccess; } bool ULFSLockProvider::UnlockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, - TArray& OutResults, TArray& OutErrorMessages) + TArray& OutSucceededFiles, TArray& OutResults, + TArray& OutErrorMessages) { - return RunLFSCommand(TEXT("unlock"), InRepositoryRoot, Params.GitBinaryPath, Params.CustomParams, Params.FileNames, - OutResults, OutErrorMessages); + const bool bSuccess = RunLFSCommand(TEXT("unlock"), InRepositoryRoot, Params.GitBinaryPath, Params.CustomParams, + Params.FileNames, OutResults, OutErrorMessages); + if (bSuccess) + { + OutSucceededFiles.Append(Params.FileNames); + } + return bSuccess; } bool ULFSLockProvider::CheckLockableExtensions(const FString& InPathToGitBinary, const FString& InRepositoryRoot, diff --git a/Source/GitSourceControl/Private/LFSLockProvider.h b/Source/GitSourceControl/Private/LFSLockProvider.h index fbce131c..bb0115bc 100644 --- a/Source/GitSourceControl/Private/LFSLockProvider.h +++ b/Source/GitSourceControl/Private/LFSLockProvider.h @@ -18,10 +18,12 @@ class ULFSLockProvider : public UGitLockProviderBase TArray& OutResults, TArray& OutErrorMessages) override; virtual bool LockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, - TArray& OutResults, TArray& OutErrorMessages) override; + TArray& OutSucceededFiles, TArray& OutResults, + TArray& OutErrorMessages) override; virtual bool UnlockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, - TArray& OutResults, TArray& OutErrorMessages) override; + TArray& OutSucceededFiles, TArray& OutResults, + TArray& OutErrorMessages) override; bool CheckLockableExtensions(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InFiles, TArray& OutLockableExtensions, diff --git a/Source/GitSourceControl/Private/ModioLockProvider.cpp b/Source/GitSourceControl/Private/ModioLockProvider.cpp index 00a82e13..8966f2b4 100644 --- a/Source/GitSourceControl/Private/ModioLockProvider.cpp +++ b/Source/GitSourceControl/Private/ModioLockProvider.cpp @@ -17,63 +17,133 @@ #include "Misc/App.h" #include "Misc/EngineVersionComparison.h" #include "Misc/Optional.h" +#include "Misc/Paths.h" #include "Serialization/JsonSerializer.h" +#include "Serialization/JsonWriter.h" + +#include + +namespace ModioLockProviderConstants +{ + /** Endpoints on the ConBot FileLock controller. */ + static const TCHAR* LocksEndpoint = TEXT("/api/FileLock/lock"); + static const TCHAR* BatchLockEndpoint = TEXT("/api/FileLock/locks"); + static const TCHAR* BatchReleaseEndpoint = TEXT("/api/FileLock/locks/release"); + + /** Per-asset outcomes returned by the batch endpoints. */ + static const TCHAR* StatusCreated = TEXT("Created"); + static const TCHAR* StatusAlreadyOwned = TEXT("AlreadyOwned"); + static const TCHAR* StatusConflict = TEXT("Conflict"); + static const TCHAR* StatusReleased = TEXT("Released"); + static const TCHAR* StatusNotLocked = TEXT("NotLocked"); + static const TCHAR* StatusNotOwned = TEXT("NotOwned"); + + static constexpr float RequestTimeoutSeconds = 30.0f; + + /** + * The HTTP delegate completes on the HTTP thread, so the calling thread polls for it. One + * request now covers a whole batch, so this granularity costs a single sleep per operation. + */ + static constexpr float PollIntervalSeconds = 0.02f; + + /** Backstop in case the request never completes and never times out on its own. */ + static constexpr double WaitLimitSeconds = RequestTimeoutSeconds + 15.0; +} + +namespace +{ + /** + * Server state is keyed by asset path, so every caller has to agree on one spelling. Unreal hands + * us '/'-separated relative paths in most cases but not all, and a '\' would additionally have to + * be escaped to survive the JSON body. + */ + FString NormalizeAssetPath(const FString& InPath) + { + FString Normalized = InPath; + FPaths::NormalizeFilename(Normalized); + return Normalized; + } + + /** The project a lock belongs to. Locks are scoped per project on the server. */ + FString GetLockProjectName() + { + return FApp::GetProjectName(); + } + + /** + * Shared between the calling thread and the HTTP thread. Held by shared pointer so that a + * response arriving after we have given up writes into live memory rather than a dead stack frame. + */ + struct FModioLockHttpResult + { + /** Release-ordered by bComplete: only read after bComplete observes true. */ + TUnion Value; + std::atomic bComplete {false}; + }; +} TSharedRef UModioLockProvider::GetLocksRequest() { FHttpModule& HttpModule = FHttpModule::Get(); TSharedRef Request = HttpModule.CreateRequest(); - FString RequestURL = ServerAddress + TEXT("/api/FileLock/lock"); + FString RequestURL = ServerAddress + ModioLockProviderConstants::LocksEndpoint; Request->SetVerb(TEXT("GET")); Request->SetURL(RequestURL); + Request->SetTimeout(ModioLockProviderConstants::RequestTimeoutSeconds); return Request; } -TSharedRef UModioLockProvider::LockFileRequest(const FString& Username, - const FString& FilePath, - const FString& ProjectName) +TSharedRef UModioLockProvider::BatchLockRequest(const FString& Endpoint, + const FString& Username, + const FString& ProjectName, + const TArray& AssetPaths, + bool bCreateProject) { FHttpModule& HttpModule = FHttpModule::Get(); TSharedRef Request = HttpModule.CreateRequest(); - FString RequestURL = ServerAddress + TEXT("/api/FileLock/lock"); Request->SetVerb(TEXT("POST")); - Request->SetURL(RequestURL); + Request->SetURL(ServerAddress + Endpoint); Request->SetHeader(TEXT("Content-Type"), TEXT("application/json")); - FString RequestContent = FString::Format(TEXT("{\"username\": \"{0}\", \"assetPath\": \"{1}\", \"projectName\": " - "\"{2}\", \"bCreateProject\": true }"), - {*Username, *FilePath, *ProjectName}); - Request->SetContentAsString(RequestContent); - return Request; -} + Request->SetTimeout(ModioLockProviderConstants::RequestTimeoutSeconds); -TSharedRef UModioLockProvider::UnlockFileRequest(const FString& Username, - const FString& FilePath, - const FString& ProjectName) -{ - FHttpModule& HttpModule = FHttpModule::Get(); + // Serialized rather than formatted into a string: asset paths and usernames are not guaranteed + // to be free of characters that need escaping, and a malformed body is a silent 400. + TSharedRef Body = MakeShared(); + Body->SetStringField(TEXT("username"), Username); + Body->SetStringField(TEXT("projectName"), ProjectName); + Body->SetBoolField(TEXT("bCreateProject"), bCreateProject); + + TArray> PathValues; + PathValues.Reserve(AssetPaths.Num()); + for (const FString& AssetPath : AssetPaths) + { + PathValues.Add(MakeShared(AssetPath)); + } + Body->SetArrayField(TEXT("assetPaths"), PathValues); + + FString RequestContent; + TSharedRef> Writer = TJsonWriterFactory<>::Create(&RequestContent); + FJsonSerializer::Serialize(Body, Writer); + Request->SetContentAsString(RequestContent); - TSharedRef Request = HttpModule.CreateRequest(); - FString RequestURL = - ServerAddress + FString::Format(TEXT("/api/FileLock/lock?username={0}&assetPath={1}&projectName={2}"), - {*Username, *FGenericPlatformHttp::UrlEncode(FilePath), *ProjectName}); - Request->SetVerb(TEXT("DELETE")); - Request->SetURL(RequestURL); return Request; } TUnion UModioLockProvider::PerformHttpRequest(TSharedRef Request) { - TUnion Result; - Result.SetSubtype(-1); + TSharedRef State = MakeShared(); + State->Value.SetSubtype(-1); + Request->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); Request->OnProcessRequestComplete().BindLambda( - [&Result](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bConnectedSuccessfully) { + [State](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bConnectedSuccessfully) { // need to check validity of request and response here if (!Request.IsValid() || !Response.IsValid()) { - Result.SetSubtype(404); + State->Value.SetSubtype(404); + State->bComplete.store(true, std::memory_order_release); return; } FTSMessageLog("SourceControl") @@ -82,25 +152,39 @@ TUnion UModioLockProvider::PerformHttpRequest(TSharedRefGetVerb(), *Request->GetURL(), Response->GetResponseCode()}))); if (Response->GetResponseCode() == 200) { - Result.SetSubtype(Response->GetContentAsString()); + State->Value.SetSubtype(Response->GetContentAsString()); } else { - Result.SetSubtype(Response->GetResponseCode()); + State->Value.SetSubtype(Response->GetResponseCode()); } + // The release store pairs with the acquire load below + State->bComplete.store(true, std::memory_order_release); }); - Request->ProcessRequest(); - double LastTime = FPlatformTime::Seconds(); - while (Request->GetStatus() == EHttpRequestStatus::Processing) + + if (!Request->ProcessRequest()) { - // const double AppTime = FPlatformTime::Seconds(); - // FHttpModule::Get().GetHttpManager().Tick(AppTime - LastTime); - // LastTime = AppTime; - FPlatformProcess::Sleep(0.1f); + // The request was rejected before it started, so the completion delegate will never fire. + TUnion Failed; + Failed.SetSubtype(-1); + return Failed; + } + + const double Deadline = FPlatformTime::Seconds() + ModioLockProviderConstants::WaitLimitSeconds; + while (!State->bComplete.load(std::memory_order_acquire)) + { + if (FPlatformTime::Seconds() > Deadline) + { + Request->CancelRequest(); + TUnion TimedOut; + TimedOut.SetSubtype(408); + // State is shared, so a late completion writes somewhere still alive. + return TimedOut; + } + FPlatformProcess::Sleep(ModioLockProviderConstants::PollIntervalSeconds); } - // synchronous event from task pool - return Result; + return State->Value; } TSharedPtr UModioLockProvider::GetResponseAsJsonObject(const FString& ResponseString) @@ -182,40 +266,135 @@ bool UModioLockProvider::GetLockedFiles(const FString& InRepositoryRoot, const F } } -bool UModioLockProvider::LockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, - TArray& OutResults, TArray& OutErrorMessages) +bool UModioLockProvider::RunBatchLockOperation(const FString& Endpoint, bool bLocking, + const FGitFileLockOpParams& Params, TArray& OutSucceededFiles, + TArray& OutResults, TArray& OutErrorMessages) { - auto Result = PerformHttpRequest(LockFileRequest(FGitSourceControlModule::Get().GetProvider().GetLockUser(), - Params.FileNames[0], FApp::GetProjectName())); - if (Result.GetCurrentSubtypeIndex() == 0) + if (Params.FileNames.Num() == 0) { - // deserialize here + // Nothing to do, and the endpoints reject an empty batch. return true; } - else + + // The server echoes the path it was sent, so keep a way back to the caller's spelling: callers + // use the returned entries to key their own caches and must get their own strings back. + TMap RequestedByAssetPath; + TArray AssetPaths; + RequestedByAssetPath.Reserve(Params.FileNames.Num()); + AssetPaths.Reserve(Params.FileNames.Num()); + for (const FString& FileName : Params.FileNames) { - OutErrorMessages.Add(FString::Format(TEXT("Request to lock file {0} resulted in HTTP error {1}"), - {*Params.FileNames[0], Result.GetSubtype()})); + const FString AssetPath = NormalizeAssetPath(FileName); + if (!RequestedByAssetPath.Contains(AssetPath)) + { + RequestedByAssetPath.Add(AssetPath, FileName); + AssetPaths.Add(AssetPath); + } + } + + const FString ProjectName = GetLockProjectName(); + const FString Username = FGitSourceControlModule::Get().GetProvider().GetLockUser(); + + auto Result = PerformHttpRequest( + BatchLockRequest(Endpoint, Username, ProjectName, AssetPaths, /*bCreateProject=*/bLocking)); + + if (Result.GetCurrentSubtypeIndex() != 0) + { + OutErrorMessages.Add(FString::Format(TEXT("Request to {0} {1} file(s) resulted in HTTP error {2}"), + {bLocking ? TEXT("lock") : TEXT("unlock"), AssetPaths.Num(), + Result.GetSubtype()})); return false; } + + return ApplyBatchResponse(Result.GetSubtype(), RequestedByAssetPath, bLocking, OutSucceededFiles, + OutResults, OutErrorMessages); } -bool UModioLockProvider::UnlockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, - TArray& OutResults, TArray& OutErrorMessages) +bool UModioLockProvider::ApplyBatchResponse(const FString& ResponseString, + const TMap& RequestedByAssetPath, bool bLocking, + TArray& OutSucceededFiles, TArray& OutResults, + TArray& OutErrorMessages) { - auto Result = PerformHttpRequest(UnlockFileRequest(FGitSourceControlModule::Get().GetProvider().GetLockUser(), - Params.FileNames[0], FApp::GetProjectName())); - if (Result.GetCurrentSubtypeIndex() == 0) + TSharedPtr ResponseObject = GetResponseAsJsonObject(ResponseString); + if (!ResponseObject.IsValid()) { - // deserialize here - return true; + OutErrorMessages.Add(TEXT("Could not parse the response from the lock server")); + return false; } - else + + const TArray>* Results = nullptr; + if (!ResponseObject->TryGetArrayField(TEXT("results"), Results) || Results == nullptr) { - OutErrorMessages.Add(FString::Format(TEXT("Request to unlock file {0} resulted in HTTP error {1}"), - {*Params.FileNames[0], Result.GetSubtype()})); + OutErrorMessages.Add(TEXT("Lock server response did not contain any per-asset results")); return false; } + + bool bAllSucceeded = true; + for (const TSharedPtr& Entry : *Results) + { + const TSharedPtr EntryObject = Entry.IsValid() ? Entry->AsObject() : nullptr; + if (!EntryObject.IsValid()) + { + continue; + } + + FString AssetPath; + FString Status; + EntryObject->TryGetStringField(TEXT("assetPath"), AssetPath); + EntryObject->TryGetStringField(TEXT("status"), Status); + + FString LockedBy; + EntryObject->TryGetStringField(TEXT("lockedBy"), LockedBy); + + // Hand back the caller's own string; an unrecognised path means the server echoed something + // we did not send, so fall back to what it told us rather than dropping the entry. + const FString* Requested = RequestedByAssetPath.Find(NormalizeAssetPath(AssetPath)); + const FString ReportedFile = Requested != nullptr ? *Requested : AssetPath; + + const bool bSucceeded = + bLocking ? (Status == ModioLockProviderConstants::StatusCreated || + Status == ModioLockProviderConstants::StatusAlreadyOwned) + : (Status == ModioLockProviderConstants::StatusReleased || + Status == ModioLockProviderConstants::StatusNotLocked); + + if (bSucceeded) + { + OutSucceededFiles.Add(ReportedFile); + OutResults.Add(FString::Format(TEXT("{0}\t{1}"), {*ReportedFile, *Status})); + continue; + } + + bAllSucceeded = false; + if (Status == ModioLockProviderConstants::StatusConflict || + Status == ModioLockProviderConstants::StatusNotOwned) + { + OutErrorMessages.Add(FString::Format(TEXT("{0} is locked by {1}"), + {*ReportedFile, LockedBy.IsEmpty() ? TEXT("another user") : *LockedBy})); + } + else + { + OutErrorMessages.Add(FString::Format(TEXT("Could not {0} {1}: the lock server reported '{2}'"), + {bLocking ? TEXT("lock") : TEXT("unlock"), *ReportedFile, *Status})); + } + } + + return bAllSucceeded; +} + +bool UModioLockProvider::LockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutSucceededFiles, TArray& OutResults, + TArray& OutErrorMessages) +{ + return RunBatchLockOperation(ModioLockProviderConstants::BatchLockEndpoint, /*bLocking=*/true, Params, + OutSucceededFiles, OutResults, OutErrorMessages); +} + +bool UModioLockProvider::UnlockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutSucceededFiles, TArray& OutResults, + TArray& OutErrorMessages) +{ + return RunBatchLockOperation(ModioLockProviderConstants::BatchReleaseEndpoint, /*bLocking=*/false, Params, + OutSucceededFiles, OutResults, OutErrorMessages); } bool UModioLockProvider::ConfigureWithSettings(const FGitLockProviderSettings& NewSettings, TArray& OutErrors) diff --git a/Source/GitSourceControl/Private/ModioLockProvider.h b/Source/GitSourceControl/Private/ModioLockProvider.h index 58106460..faccdc03 100644 --- a/Source/GitSourceControl/Private/ModioLockProvider.h +++ b/Source/GitSourceControl/Private/ModioLockProvider.h @@ -18,18 +18,26 @@ class UModioLockProvider : public UGitLockProviderBase { GENERATED_BODY() TSharedRef GetLocksRequest(); - TSharedRef LockFileRequest(const FString& Username, - const FString& FilePath, - const FString& ProjectName); - TSharedRef UnlockFileRequest(const FString& Username, - const FString& FilePath, - const FString& ProjectName); + + TSharedRef BatchLockRequest(const FString& Endpoint, + const FString& Username, + const FString& ProjectName, + const TArray& AssetPaths, + bool bCreateProject); TUnion PerformHttpRequest(TSharedRef Request); TSharedPtr GetResponseAsJsonObject(const FString& ResponseString); TArray> GetResponseAsJsonArray(const FString& ResponseString); + bool RunBatchLockOperation(const FString& Endpoint, bool bLocking, const FGitFileLockOpParams& Params, + TArray& OutSucceededFiles, TArray& OutResults, + TArray& OutErrorMessages); + + bool ApplyBatchResponse(const FString& ResponseString, const TMap& RequestedByAssetPath, + bool bLocking, TArray& OutSucceededFiles, TArray& OutResults, + TArray& OutErrorMessages); + void YieldThread(); FString ServerAddress; @@ -41,10 +49,12 @@ class UModioLockProvider : public UGitLockProviderBase bool GetLockedFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, TArray& OutResults, TArray& OutErrorMessages) override; - bool LockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, TArray& OutResults, + bool LockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutSucceededFiles, TArray& OutResults, TArray& OutErrorMessages) override; - bool UnlockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, TArray& OutResults, + bool UnlockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutSucceededFiles, TArray& OutResults, TArray& OutErrorMessages) override; bool ConfigureWithSettings(const FGitLockProviderSettings& NewSettings, TArray& OutErrors) override; diff --git a/Source/GitSourceControl/Public/IGitLockProvider.h b/Source/GitSourceControl/Public/IGitLockProvider.h index 893b3c63..57cd5364 100644 --- a/Source/GitSourceControl/Public/IGitLockProvider.h +++ b/Source/GitSourceControl/Public/IGitLockProvider.h @@ -23,16 +23,23 @@ class IGitLockProvider GENERATED_BODY() public: virtual bool ConfigureWithSettings(const FGitLockProviderSettings& NewSettings, TArray& OutErrors) = 0; + virtual bool RunLFSCommand(const FString& InCommand, const FString& InRepositoryRoot, const FString& GitBinaryFallback, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages) = 0; + virtual bool GetLockedFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, TArray& OutResults, TArray& OutErrorMessages) = 0; + virtual bool LockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, - TArray& OutResults, TArray& OutErrorMessages) = 0; + TArray& OutSucceededFiles, TArray& OutResults, + TArray& OutErrorMessages) = 0; + virtual bool UnlockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, - TArray& OutResults, TArray& OutErrorMessages) = 0; + TArray& OutSucceededFiles, TArray& OutResults, + TArray& OutErrorMessages) = 0; + virtual bool CheckLockableExtensions(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InFiles, TArray& OutLockableExtensions, TArray& OutErrorMessages) = 0; @@ -56,13 +63,15 @@ class UGitLockProviderBase : public UObject, public IGitLockProvider return false; } - bool LockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, TArray& OutResults, + bool LockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutSucceededFiles, TArray& OutResults, TArray& OutErrorMessages) override { return false; } - bool UnlockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, TArray& OutResults, + bool UnlockFiles(const FString& InRepositoryRoot, const FGitFileLockOpParams& Params, + TArray& OutSucceededFiles, TArray& OutResults, TArray& OutErrorMessages) override { return false;