diff --git a/Plugins/UE4GitPlugin-2.17-beta/.gitbugtraq b/Plugins/UE4GitPlugin-2.17-beta/.gitbugtraq
new file mode 100644
index 0000000..e47406d
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/.gitbugtraq
@@ -0,0 +1,7 @@
+# .gitbugtraq for Git GUIs (SmartGit/TortoiseGit) to show links to the Github issue tracker.
+# Instead of the repository root directory, it could be added as an additional section to $GIT_DIR/config.
+# (note that '\' need to be escaped).
+[bugtraq]
+ url = https://github.com/SRombauts/UE4GitPlugin/issues/%BUGID%
+ loglinkregex = "#\\d+"
+ logregex = \\d+
diff --git a/Plugins/UE4GitPlugin-2.17-beta/.gitignore b/Plugins/UE4GitPlugin-2.17-beta/.gitignore
new file mode 100644
index 0000000..2ca2cfb
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/.gitignore
@@ -0,0 +1,5 @@
+/Binaries/*/*.pdb
+/Binaries/*/*Debug*
+/Binaries/*/*.dylib
+/Binaries/*/*.modules
+/Intermediate
diff --git a/Plugins/UE4GitPlugin-2.17-beta/GitSourceControl.uplugin b/Plugins/UE4GitPlugin-2.17-beta/GitSourceControl.uplugin
new file mode 100644
index 0000000..661c4b8
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/GitSourceControl.uplugin
@@ -0,0 +1,25 @@
+{
+ "FileVersion" : 3,
+ "Version" : 37,
+ "VersionName" : "2.17",
+ "FriendlyName" : "Git LFS 2",
+ "Description" : "Git source control management (dev)",
+ "Category" : "Source Control",
+ "CreatedBy" : "SRombauts",
+ "CreatedByURL" : "http://srombauts.github.com",
+ "DocsURL" : "",
+ "MarketplaceURL" : "",
+ "SupportURL" : "",
+ "EnabledByDefault" : true,
+ "CanContainContent" : false,
+ "IsBetaVersion" : true,
+ "Installed" : false,
+ "Modules" :
+ [
+ {
+ "Name" : "GitSourceControl",
+ "Type" : "Editor",
+ "LoadingPhase" : "Default"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Plugins/UE4GitPlugin-2.17-beta/LICENSE.txt b/Plugins/UE4GitPlugin-2.17-beta/LICENSE.txt
new file mode 100644
index 0000000..4cdc13c
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/LICENSE.txt
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/Plugins/UE4GitPlugin-2.17-beta/README.md b/Plugins/UE4GitPlugin-2.17-beta/README.md
new file mode 100644
index 0000000..4c0ce4d
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/README.md
@@ -0,0 +1,201 @@
+Unreal Engine 4 Git Source Control Plugin
+-----------------------------------------
+
+[![release](https://img.shields.io/github/release/SRombauts/UE4GitPlugin.svg)](https://github.com/SRombauts/UE4GitPlugin/releases)
+[![Git Plugin issues](https://img.shields.io/github/issues/SRombauts/UE4GitPlugin.svg)](https://github.com/SRombauts/UE4GitPlugin/issues)
+[![Join the chat at https://gitter.im/SRombauts/UE4GitPlugin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/SRombauts/UE4GitPlugin)
+UE4GitPlugin is a simple Git Source Control Plugin for **Unreal Engine 4.26**.
+
+Developed and contributed by Sébastien Rombauts 2014-2020 (sebastien.rombauts@gmail.com)
+
+
+- First version of the plugin has been **integrated by default in UE4.7 in "beta version"**.
+- This is a developement fork named "**Git LFS 2**" adding File Locks supported by Github.
+
+You need to install it into your Project **Plugins/** folder, and it will overwrite (replace) the default "Git (beta version)" Source Control Provider with the "Git LFS 2" plugin.
+
+Have a look at the [Git Plugin Tutorial on the Wiki](https://wiki.unrealengine.com/Git_source_control_%28Tutorial%29). ([alternate link](https://michaeljcole.github.io/wiki.unrealengine.com/Git_source_control_%28Tutorial%29/))
+
+Written and contributed by Sebastien Rombauts (sebastien.rombauts@gmail.com)
+
+Source Control Login window to create a new workspace/a new repository:
+![Source Control Login window - create a new repository](Screenshots/SourceControlLogin_Init.png)
+
+Source Control status tooltip, when hovering the Source Control icon in toolbar:
+![Source Control Status Tooltip](Screenshots/SourceControlStatusTooltip.png)
+
+Source Control top Menu, extended with a few commands specific to Git:
+![Source Control Status Tooltip](Screenshots/SourceControlMenu.png)
+
+Submit Files to Source Control window, to commit assets:
+![Submit Files to Source Control](Screenshots/SubmitFiles.png)
+
+File History window, to see the changelog of an asset:
+![History of a file](Screenshots/FileHistory.png)
+
+Visual Diffing of two revisions of a Blueprint:
+
+
+Merge conflict of a Blueprint:
+
+
+Status Icons:
+
+![New/Unsaved/Untracked](Screenshots/Icons/New.png)
+![Added](Screenshots/Icons/Added.png)
+![Unchanged](Screenshots/Icons/Unchanged.png)
+![Modified](Screenshots/Icons/Modified.png)
+![Moved/Renamed](Screenshots/Icons/Renamed.png)
+
+### Supported features
+- initialize a new Git local repository ('git init') to manage your UE4 Game Project
+ - can also create an appropriate .gitignore file as part of initialization
+ - can also create a .gitattributes file to enable Git LFS (Large File System) as part of initialization
+ - can also enable Git LFS 2.x File Locks as part of initialization
+ - can also make the initial commit, with custom multi-line message
+- display status icons to show modified/added/deleted/untracked files, not at head and conflicted
+- show history of a file
+- visual diff of a blueprint against depot or between previous versions of a file
+- revert modifications of a file (works best with "Content Hot-Reload" experimental option of UE4.15, by default since 4.16)
+- add, delete, rename a file
+- checkin/commit a file (cannot handle atomically more than 50 files)
+- migrate an asset between two projects if both are using Git
+- solve a merge conflict on a blueprint
+- show current branch name in status text
+- Configure remote origin URL ('git remote add origin url')
+- Sync to Pull (rebase) the current branch if there is no local modified files
+- Push the current branch
+- Git LFS (Github, Gitlab, Bitbucket), git-annex, git-fat and git-media are working with Git 2.10+
+- Git LFS 2 File Locks
+- Windows, Mac and Linux
+
+### What *cannot* be done presently
+- Branch/Merge are not in the current Editor workflow
+- Amend a commit is not in the current Editor workflow
+- Revert All (using either "Stash" or "reset --hard")
+- Configure user name & email ('git config user.name' & git config user.email')
+- Authentication is not managed if needed for Sync (Pull)
+
+### Known issues
+- #34 "outside repository" fatal error
+- #37 Rebase workflow: conflicts not detected!
+- #41 UE-44637: Deleting an asset is unsuccessful if the asset is marked for add (since UE4.13)
+- #46 Merge Conflicts - Accept Target - causes engine to crash bug
+- #47 Git LFS conflict resolution not working
+- #49 Git LFS 2: False error in logs after a successful push
+- #51 Git LFS 2: cannot revert a modified/unchecked-out asset
+- #53 Git LFS 2: document the configuration and workflow
+- #54 Poor performances of 'lfs locks' on Windows command line
+- #55 Git LFS 2: Unlocking a renamed asset
+
+- missing localisation for git specific messages
+- displaying states of 'Engine' assets (also needs management of 'out of tree' files)
+- renaming a Blueprint in Editor leaves a redirector file, AND modify too much the asset to enable git to track its history through renaming
+
+### Getting started
+
+Quick demo of the Git Plugin on Unreal Engine 4.12 (preview)
+[![Git Plugin on Unreal Engine 4.12 (preview)](https://img.youtube.com/vi/rRhPl9vL58Q/0.jpg)](https://youtu.be/rRhPl9vL58Q)
+
+#### Install Git
+
+Under Windows 64bits, you should install the standard standalone Git for Windows
+(now comming with Git LFS 2 with File Locking) with default parameters,
+usually in "C:\Program Files\Git\bin\git.exe".
+
+Then you have to configure your name and e-mail that will appear in each of your commits:
+
+```
+git config --global user.name "Sébastien Rombauts"
+git config --global user.email sebastien.rombauts@gmail.com
+```
+
+#### Install this Git Plugin (dev) into your Game Project
+
+Unreal Engine comes with a stable version of this plugin, so no need to install it.
+
+This alternate "Git development plugin" needs to be installed into a subfolder or your Game Project "Plugins" directory
+(that is, you cannot install it into the Engine Plugins directory):
+
+```
+/Plugins
+```
+
+You will obviously only be able to use the plugin within this project.
+
+See also the [Plugins official Documentation](https://docs.unrealengine.com/latest/INT/Programming/Plugins/index.html)
+
+#### Activate Git Source Control for your Game Project
+
+Load your Game Project in Unreal Engine, then open:
+
+```
+File->Connect To Source Control... -> Git
+```
+
+##### Project already managed by Git
+
+If your project is already under Git (it contains a ".git" subfolder), just click on "Accept Settings". This connect the Editor to your local Git repository ("Depot").
+
+##### Project not already under Git
+
+Otherwise, the Git Plugin is able to create (initialize) a new local Git Repository with your project Assets and Sources files:
+
+
+
+Click "Initialize project with Git" that will add all relevant files to source control and make the initial commit with the customizable message.
+When everything is done, click on "Accept Settings".
+
+#### Using the Git Source Control Provider in the Unreal Engine Editor
+
+The plugin mostly interacts with you local Git repository ("Depot"), not much with the remote server (usually "origin").
+
+It displays Git status icons on top of assets in the Asset Browser:
+- No icon means that the file is under source control and unchanged since last commit, or ignored.
+- A red mark is for "modified" assets, that is the one that needs to be committed (so not the same as "Check-out" in Perforce/SVN/Plastic SCM).
+- A red cross is for "added" assets, that also needs to be committed
+- A blue lightning means "renamed".
+- A yellow exclamation point is for files in conflict after a merge, or is not at head (latest revision on the current remote branch).
+- A yellow question mark is for files not in source control.
+
+TODO:
+- specifics of rename and redirectors, and "Fix Up Redirector in Folder" command
+- history / visual diff
+- CheckIn = Commit
+- CheckOut = Commit+Push+unlock (when using LFS 2)
+
+See also the [Source Control official Documentation](https://docs.unrealengine.com/latest/INT/Engine/UI/SourceControl/index.html)
+
+### License
+
+Copyright (c) 2014-2020 Sébastien Rombauts (sebastien.rombauts@gmail.com)
+
+Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
+or copy at http://opensource.org/licenses/MIT)
+
+## How to contribute
+### GitHub website
+The most efficient way to help and contribute to this wrapper project is to
+use the tools provided by GitHub:
+- please fill bug reports and feature requests here: https://github.com/SRombauts/UE4GitPlugin/issues
+- fork the repository, make some small changes and submit them with independent pull-requests
+
+### Contact
+- You can use the Unreal Engine forums.
+- You can also email me directly, I will answer any questions and requests.
+
+### Coding Style Guidelines
+The source code follow the UnreaEngine official [Coding Standard](https://docs.unrealengine.com/latest/INT/Programming/Development/CodingStandard/index.html):
+- CamelCase naming convention, with a prefix letter to differentiate classes ('F'), interfaces ('I'), templates ('T')
+- files (.cpp/.h) are named like the class they contains
+- Doxygen comments, documentation is located with declaration, on headers
+- Use portable common features of C++11 like nullptr, auto, range based for, override keyword
+- Braces on their own line
+- Tabs to indent code, with a width of 4 characters
+
+## See also
+
+- [Git Source Control Tutorial on the Wikis](https://wiki.unrealengine.com/Git_source_control_(Tutorial))
+- [UE4 Git Plugin website](http://srombauts.github.com/UE4GitPlugin)
+
+- [ue4-hg-plugin for Mercurial (and bigfiles)](https://github.com/enlight/ue4-hg-plugin)
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Resources/Icon128.png b/Plugins/UE4GitPlugin-2.17-beta/Resources/Icon128.png
new file mode 100644
index 0000000..3d9e864
Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Resources/Icon128.png differ
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/FileHistory.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/FileHistory.png
new file mode 100644
index 0000000..2b9e0f5
Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/FileHistory.png differ
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Added.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Added.png
new file mode 100644
index 0000000..fff4f19
Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Added.png differ
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Modified.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Modified.png
new file mode 100644
index 0000000..a7f7789
Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Modified.png differ
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/New.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/New.png
new file mode 100644
index 0000000..cfea784
Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/New.png differ
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Renamed.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Renamed.png
new file mode 100644
index 0000000..d66b81b
Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Renamed.png differ
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Unchanged.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Unchanged.png
new file mode 100644
index 0000000..17307ad
Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Unchanged.png differ
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SourceControlLogin_Init.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SourceControlLogin_Init.png
new file mode 100644
index 0000000..3b2f1c3
Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SourceControlLogin_Init.png differ
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SourceControlMenu.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SourceControlMenu.png
new file mode 100644
index 0000000..d4f85b7
Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SourceControlMenu.png differ
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SourceControlStatusTooltip.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SourceControlStatusTooltip.png
new file mode 100644
index 0000000..ef36f1f
Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SourceControlStatusTooltip.png differ
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SubmitFiles.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SubmitFiles.png
new file mode 100644
index 0000000..2545aae
Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SubmitFiles.png differ
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/GitSourceControl.Build.cs b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/GitSourceControl.Build.cs
new file mode 100644
index 0000000..d16ebb3
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/GitSourceControl.Build.cs
@@ -0,0 +1,33 @@
+// 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)
+
+using UnrealBuildTool;
+
+public class GitSourceControl : ModuleRules
+{
+ public GitSourceControl(ReadOnlyTargetRules Target) : base(Target)
+ {
+ // Enable the Include-What-You-Use (IWYU) UE4.15 policy (see https://docs.unrealengine.com/en-us/Programming/UnrealBuildSystem/IWYUReferenceGuide)
+ // "Shared PCHs may be used if an explicit private PCH is not set through PrivatePCHHeaderFile. In either case, none of the source files manually include a module PCH, and should include a matching header instead."
+ bEnforceIWYU = true;
+ PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
+ PrivatePCHHeaderFile = "Private/GitSourceControlPrivatePCH.h";
+
+ PrivateDependencyModuleNames.AddRange(
+ new string[] {
+ "Core",
+ "CoreUObject",
+ "Slate",
+ "SlateCore",
+ "InputCore",
+ "DesktopWidgets",
+ "EditorStyle",
+ "UnrealEd",
+ "SourceControl",
+ "Projects",
+ }
+ );
+ }
+}
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlCommand.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlCommand.cpp
new file mode 100644
index 0000000..1bf25ec
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlCommand.cpp
@@ -0,0 +1,65 @@
+// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com)
+//
+// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
+// or copy at http://opensource.org/licenses/MIT)
+
+#include "GitSourceControlCommand.h"
+
+#include "Modules/ModuleManager.h"
+#include "GitSourceControlModule.h"
+
+FGitSourceControlCommand::FGitSourceControlCommand(const TSharedRef& InOperation, const TSharedRef& InWorker, const FSourceControlOperationComplete& InOperationCompleteDelegate)
+ : Operation(InOperation)
+ , Worker(InWorker)
+ , OperationCompleteDelegate(InOperationCompleteDelegate)
+ , bExecuteProcessed(0)
+ , bCommandSuccessful(false)
+ , bConnectionDropped(false)
+ , bAutoDelete(true)
+ , Concurrency(EConcurrency::Synchronous)
+{
+ // grab the providers settings here, so we don't access them once the worker thread is launched
+ check(IsInGameThread());
+ const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked( "GitSourceControl" );
+ PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
+ bUsingGitLfsLocking = GitSourceControl.AccessSettings().IsUsingGitLfsLocking();
+ PathToRepositoryRoot = GitSourceControl.GetProvider().GetPathToRepositoryRoot();
+}
+
+bool FGitSourceControlCommand::DoWork()
+{
+ bCommandSuccessful = Worker->Execute(*this);
+ FPlatformAtomics::InterlockedExchange(&bExecuteProcessed, 1);
+
+ return bCommandSuccessful;
+}
+
+void FGitSourceControlCommand::Abandon()
+{
+ FPlatformAtomics::InterlockedExchange(&bExecuteProcessed, 1);
+}
+
+void FGitSourceControlCommand::DoThreadedWork()
+{
+ Concurrency = EConcurrency::Asynchronous;
+ DoWork();
+}
+
+ECommandResult::Type FGitSourceControlCommand::ReturnResults()
+{
+ // Save any messages that have accumulated
+ for (FString& String : InfoMessages)
+ {
+ Operation->AddInfoMessge(FText::FromString(String));
+ }
+ for (FString& String : ErrorMessages)
+ {
+ Operation->AddErrorMessge(FText::FromString(String));
+ }
+
+ // run the completion delegate if we have one bound
+ ECommandResult::Type Result = bCommandSuccessful ? ECommandResult::Succeeded : ECommandResult::Failed;
+ OperationCompleteDelegate.ExecuteIfBound(Operation, Result);
+
+ return Result;
+}
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlCommand.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlCommand.h
new file mode 100644
index 0000000..42c1a89
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlCommand.h
@@ -0,0 +1,92 @@
+// 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 "CoreMinimal.h"
+#include "ISourceControlProvider.h"
+#include "Misc/IQueuedWork.h"
+
+/**
+ * Used to execute Git commands multi-threaded.
+ */
+class FGitSourceControlCommand : public IQueuedWork
+{
+public:
+
+ FGitSourceControlCommand(const TSharedRef& InOperation, const TSharedRef& InWorker, const FSourceControlOperationComplete& InOperationCompleteDelegate = FSourceControlOperationComplete() );
+
+ /**
+ * This is where the real thread work is done. All work that is done for
+ * this queued object should be done from within the call to this function.
+ */
+ bool DoWork();
+
+ /**
+ * Tells the queued work that it is being abandoned so that it can do
+ * per object clean up as needed. This will only be called if it is being
+ * abandoned before completion. NOTE: This requires the object to delete
+ * itself using whatever heap it was allocated in.
+ */
+ virtual void Abandon() override;
+
+ /**
+ * This method is also used to tell the object to cleanup but not before
+ * the object has finished it's work.
+ */
+ virtual void DoThreadedWork() override;
+
+ /** Save any results and call any registered callbacks. */
+ ECommandResult::Type ReturnResults();
+
+public:
+ /** Path to the Git binary */
+ FString PathToGitBinary;
+
+ /** Path to the root of the Git repository: can be the ProjectDir itself, or any parent directory (found by the "Connect" operation) */
+ FString PathToRepositoryRoot;
+
+ /** Tell if using the Git LFS file Locking workflow */
+ bool bUsingGitLfsLocking;
+
+ /** Operation we want to perform - contains outward-facing parameters & results */
+ TSharedRef Operation;
+
+ /** The object that will actually do the work */
+ TSharedRef Worker;
+
+ /** Delegate to notify when this operation completes */
+ FSourceControlOperationComplete OperationCompleteDelegate;
+
+ /**If true, this command has been processed by the source control thread*/
+ volatile int32 bExecuteProcessed;
+
+ /**If true, the source control command succeeded*/
+ bool bCommandSuccessful;
+
+ /** TODO LFS If true, the source control connection was dropped while this command was being executed*/
+ bool bConnectionDropped;
+
+ /** Current Commit full SHA1 */
+ FString CommitId;
+
+ /** Current Commit description's Summary */
+ FString CommitSummary;
+
+ /** If true, this command will be automatically cleaned up in Tick() */
+ bool bAutoDelete;
+
+ /** Whether we are running multi-treaded or not*/
+ EConcurrency::Type Concurrency;
+
+ /** Files to perform this operation on */
+ TArray Files;
+
+ /**Info and/or warning message storage*/
+ TArray InfoMessages;
+
+ /**Potential error message storage*/
+ TArray ErrorMessages;
+};
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlMenu.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlMenu.cpp
new file mode 100644
index 0000000..faeea9d
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlMenu.cpp
@@ -0,0 +1,515 @@
+// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com)
+//
+// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
+// or copy at http://opensource.org/licenses/MIT)
+
+#include "GitSourceControlMenu.h"
+
+#include "GitSourceControlModule.h"
+#include "GitSourceControlProvider.h"
+#include "GitSourceControlOperations.h"
+#include "GitSourceControlUtils.h"
+
+#include "ISourceControlModule.h"
+#include "ISourceControlOperation.h"
+#include "SourceControlOperations.h"
+
+#include "LevelEditor.h"
+#include "Widgets/Notifications/SNotificationList.h"
+#include "Framework/Notifications/NotificationManager.h"
+#include "Framework/MultiBox/MultiBoxBuilder.h"
+#include "Misc/MessageDialog.h"
+#include "EditorStyleSet.h"
+
+#include "PackageTools.h"
+#include "FileHelpers.h"
+
+#include "Logging/MessageLog.h"
+
+static const FName GitSourceControlMenuTabName("GitSourceControlMenu");
+
+#define LOCTEXT_NAMESPACE "GitSourceControl"
+
+void FGitSourceControlMenu::Register()
+{
+ // Register the extension with the level editor
+ FLevelEditorModule* LevelEditorModule = FModuleManager::GetModulePtr("LevelEditor");
+ if (LevelEditorModule)
+ {
+ FLevelEditorModule::FLevelEditorMenuExtender ViewMenuExtender = FLevelEditorModule::FLevelEditorMenuExtender::CreateRaw(this, &FGitSourceControlMenu::OnExtendLevelEditorViewMenu);
+ auto& MenuExtenders = LevelEditorModule->GetAllLevelEditorToolbarSourceControlMenuExtenders();
+ MenuExtenders.Add(ViewMenuExtender);
+ ViewMenuExtenderHandle = MenuExtenders.Last().GetHandle();
+ }
+}
+
+void FGitSourceControlMenu::Unregister()
+{
+ // Unregister the level editor extensions
+ FLevelEditorModule* LevelEditorModule = FModuleManager::GetModulePtr("LevelEditor");
+ if (LevelEditorModule)
+ {
+ LevelEditorModule->GetAllLevelEditorToolbarSourceControlMenuExtenders().RemoveAll([=](const FLevelEditorModule::FLevelEditorMenuExtender& Extender) { return Extender.GetHandle() == ViewMenuExtenderHandle; });
+ }
+}
+
+bool FGitSourceControlMenu::HaveRemoteUrl() const
+{
+ const FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl");
+ const FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
+ return !Provider.GetRemoteUrl().IsEmpty();
+}
+
+/// Prompt to save or discard all packages
+bool FGitSourceControlMenu::SaveDirtyPackages()
+{
+ const bool bPromptUserToSave = true;
+ const bool bSaveMapPackages = true;
+ const bool bSaveContentPackages = true;
+ const bool bFastSave = false;
+ const bool bNotifyNoPackagesSaved = false;
+ const bool bCanBeDeclined = true; // If the user clicks "don't save" this will continue and lose their changes
+ bool bHadPackagesToSave = false;
+
+ bool bSaved = FEditorFileUtils::SaveDirtyPackages(bPromptUserToSave, bSaveMapPackages, bSaveContentPackages, bFastSave, bNotifyNoPackagesSaved, bCanBeDeclined, &bHadPackagesToSave);
+
+ // bSaved can be true if the user selects to not save an asset by unchecking it and clicking "save"
+ if (bSaved)
+ {
+ TArray DirtyPackages;
+ FEditorFileUtils::GetDirtyWorldPackages(DirtyPackages);
+ FEditorFileUtils::GetDirtyContentPackages(DirtyPackages);
+ bSaved = DirtyPackages.Num() == 0;
+ }
+
+ return bSaved;
+}
+
+/// Find all packages in Content directory
+TArray FGitSourceControlMenu::ListAllPackages()
+{
+ TArray PackageRelativePaths;
+ FPackageName::FindPackagesInDirectory(PackageRelativePaths, *FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()));
+
+ TArray PackageNames;
+ PackageNames.Reserve(PackageRelativePaths.Num());
+ for (const FString& Path : PackageRelativePaths)
+ {
+ FString PackageName;
+ FString FailureReason;
+ if (FPackageName::TryConvertFilenameToLongPackageName(Path, PackageName, &FailureReason))
+ {
+ PackageNames.Add(PackageName);
+ }
+ else
+ {
+ FMessageLog("SourceControl").Error(FText::FromString(FailureReason));
+ }
+ }
+
+ return PackageNames;
+}
+
+/// Unkink all loaded packages to allow to update them
+TArray FGitSourceControlMenu::UnlinkPackages(const TArray& InPackageNames)
+{
+ TArray LoadedPackages;
+
+ // Inspired from ContentBrowserUtils::SyncPathsFromSourceControl()
+ if (InPackageNames.Num() > 0)
+ {
+ // Form a list of loaded packages to reload...
+ LoadedPackages.Reserve(InPackageNames.Num());
+ for (const FString& PackageName : InPackageNames)
+ {
+ UPackage* Package = FindPackage(nullptr, *PackageName);
+ if (Package)
+ {
+ 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);
+ }
+ }
+ UE_LOG(LogSourceControl, Log, TEXT("Reseted Loader for %d Packages"), LoadedPackages.Num());
+ }
+
+ return LoadedPackages;
+}
+
+void FGitSourceControlMenu::ReloadPackages(TArray& InPackagesToReload)
+{
+ UE_LOG(LogSourceControl, Log, TEXT("Reloading %d Packages..."), InPackagesToReload.Num());
+
+ // 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);
+
+ // Unload any deleted packages...
+ UPackageTools::UnloadPackages(PackagesToUnload);
+}
+
+// Ask the user if he wants to stash any modification and try to unstash them afterward, which could lead to conflicts
+bool FGitSourceControlMenu::StashAwayAnyModifications()
+{
+ bool bStashOk = true;
+
+ FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl");
+ FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
+ const FString& PathToRespositoryRoot = Provider.GetPathToRepositoryRoot();
+ const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
+ const TArray ParametersStatus{"--porcelain --untracked-files=no"};
+ TArray InfoMessages;
+ TArray ErrorMessages;
+ // Check if there is any modification to the working tree
+ const bool bStatusOk = GitSourceControlUtils::RunCommand(TEXT("status"), PathToGitBinary, PathToRespositoryRoot, ParametersStatus, TArray(), InfoMessages, ErrorMessages);
+ if ((bStatusOk) && (InfoMessages.Num() > 0))
+ {
+ // Ask the user before stashing
+ const FText DialogText(LOCTEXT("SourceControlMenu_Stash_Ask", "Stash (save) all modifications of the working tree? Required to Sync/Pull!"));
+ const EAppReturnType::Type Choice = FMessageDialog::Open(EAppMsgType::OkCancel, DialogText);
+ if (Choice == EAppReturnType::Ok)
+ {
+ const TArray ParametersStash{ "save \"Stashed by Unreal Engine Git Plugin\"" };
+ bStashMadeBeforeSync = GitSourceControlUtils::RunCommand(TEXT("stash"), PathToGitBinary, PathToRespositoryRoot, ParametersStash, TArray(), InfoMessages, ErrorMessages);
+ if (!bStashMadeBeforeSync)
+ {
+ FMessageLog SourceControlLog("SourceControl");
+ SourceControlLog.Warning(LOCTEXT("SourceControlMenu_StashFailed", "Stashing away modifications failed!"));
+ SourceControlLog.Notify();
+ }
+ }
+ else
+ {
+ bStashOk = false;
+ }
+ }
+
+ return bStashOk;
+}
+
+// Unstash any modifications if a stash was made at the beginning of the Sync operation
+void FGitSourceControlMenu::ReApplyStashedModifications()
+{
+ if (bStashMadeBeforeSync)
+ {
+ FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl");
+ FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
+ const FString& PathToRespositoryRoot = Provider.GetPathToRepositoryRoot();
+ const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
+ const TArray ParametersStash{ "pop" };
+ TArray InfoMessages;
+ TArray ErrorMessages;
+ const bool bUnstashOk = GitSourceControlUtils::RunCommand(TEXT("stash"), PathToGitBinary, PathToRespositoryRoot, ParametersStash, TArray(), InfoMessages, ErrorMessages);
+ if (!bUnstashOk)
+ {
+ FMessageLog SourceControlLog("SourceControl");
+ SourceControlLog.Warning(LOCTEXT("SourceControlMenu_UnstashFailed", "Unstashing previously saved modifications failed!"));
+ SourceControlLog.Notify();
+ }
+ }
+}
+
+void FGitSourceControlMenu::SyncClicked()
+{
+ if (!OperationInProgressNotification.IsValid())
+ {
+ // Ask the user to save any dirty assets opened in Editor
+ const bool bSaved = SaveDirtyPackages();
+ if (bSaved)
+ {
+ // Find and Unlink all packages in Content directory to allow to update them
+ PackagesToReload = UnlinkPackages(ListAllPackages());
+
+ // Ask the user if he wants to stash any modification and try to unstash them afterward, which could lead to conflicts
+ const bool bStashed = StashAwayAnyModifications();
+ if (bStashed)
+ {
+ // Launch a "Sync" operation
+ FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl");
+ FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
+ TSharedRef SyncOperation = ISourceControlOperation::Create();
+ const ECommandResult::Type Result = Provider.Execute(SyncOperation, TArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete));
+ if (Result == ECommandResult::Succeeded)
+ {
+ // Display an ongoing notification during the whole operation (packages will be reloaded at the completion of the operation)
+ DisplayInProgressNotification(SyncOperation->GetInProgressString());
+ }
+ else
+ {
+ // Report failure with a notification and Reload all packages
+ DisplayFailureNotification(SyncOperation->GetName());
+ ReloadPackages(PackagesToReload);
+ }
+ }
+ else
+ {
+ FMessageLog SourceControlLog("SourceControl");
+ SourceControlLog.Warning(LOCTEXT("SourceControlMenu_Sync_Unsaved", "Stash away all modifications before attempting to Sync!"));
+ SourceControlLog.Notify();
+ }
+ }
+ else
+ {
+ FMessageLog SourceControlLog("SourceControl");
+ SourceControlLog.Warning(LOCTEXT("SourceControlMenu_Sync_Unsaved", "Save All Assets before attempting to Sync!"));
+ SourceControlLog.Notify();
+ }
+ }
+ else
+ {
+ FMessageLog SourceControlLog("SourceControl");
+ SourceControlLog.Warning(LOCTEXT("SourceControlMenu_InProgress", "Source control operation already in progress"));
+ SourceControlLog.Notify();
+ }
+}
+
+void FGitSourceControlMenu::PushClicked()
+{
+ if (!OperationInProgressNotification.IsValid())
+ {
+ // Launch a "Push" Operation
+ FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl");
+ FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
+ TSharedRef PushOperation = ISourceControlOperation::Create();
+ const ECommandResult::Type Result = Provider.Execute(PushOperation, TArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete));
+ if (Result == ECommandResult::Succeeded)
+ {
+ // Display an ongoing notification during the whole operation
+ DisplayInProgressNotification(PushOperation->GetInProgressString());
+ }
+ else
+ {
+ // Report failure with a notification
+ DisplayFailureNotification(PushOperation->GetName());
+ }
+ }
+ else
+ {
+ FMessageLog SourceControlLog("SourceControl");
+ SourceControlLog.Warning(LOCTEXT("SourceControlMenu_InProgress", "Source control operation already in progress"));
+ SourceControlLog.Notify();
+ }
+}
+
+void FGitSourceControlMenu::RevertClicked()
+{
+ if (!OperationInProgressNotification.IsValid())
+ {
+ // Ask the user before reverting all!
+ const FText DialogText(LOCTEXT("SourceControlMenu_Revert_Ask", "Revert all modifications of the working tree?"));
+ const EAppReturnType::Type Choice = FMessageDialog::Open(EAppMsgType::OkCancel, DialogText);
+ if (Choice == EAppReturnType::Ok)
+ {
+ // NOTE No need to force the user to SaveDirtyPackages(); since he will be presented with a choice by the Editor
+
+ // Find and Unlink all packages in Content directory to allow to update them
+ PackagesToReload = UnlinkPackages(ListAllPackages());
+
+ // Launch a "Revert" Operation
+ FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl");
+ FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
+ TSharedRef RevertOperation = ISourceControlOperation::Create();
+ const ECommandResult::Type Result = Provider.Execute(RevertOperation, TArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete));
+ if (Result == ECommandResult::Succeeded)
+ {
+ // Display an ongoing notification during the whole operation
+ DisplayInProgressNotification(RevertOperation->GetInProgressString());
+ }
+ else
+ {
+ // Report failure with a notification and Reload all packages
+ DisplayFailureNotification(RevertOperation->GetName());
+ ReloadPackages(PackagesToReload);
+ }
+ }
+ }
+ else
+ {
+ FMessageLog SourceControlLog("SourceControl");
+ SourceControlLog.Warning(LOCTEXT("SourceControlMenu_InProgress", "Source control operation already in progress"));
+ SourceControlLog.Notify();
+ }
+}
+
+void FGitSourceControlMenu::RefreshClicked()
+{
+ if (!OperationInProgressNotification.IsValid())
+ {
+ // Launch an "UpdateStatus" Operation
+ FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl");
+ FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
+ TSharedRef RefreshOperation = ISourceControlOperation::Create();
+ RefreshOperation->SetCheckingAllFiles(true);
+ const ECommandResult::Type Result = Provider.Execute(RefreshOperation, TArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete));
+ if (Result == ECommandResult::Succeeded)
+ {
+ // Display an ongoing notification during the whole operation
+ DisplayInProgressNotification(RefreshOperation->GetInProgressString());
+ }
+ else
+ {
+ // Report failure with a notification
+ DisplayFailureNotification(RefreshOperation->GetName());
+ }
+ }
+ else
+ {
+ FMessageLog SourceControlLog("SourceControl");
+ SourceControlLog.Warning(LOCTEXT("SourceControlMenu_InProgress", "Source control operation already in progress"));
+ SourceControlLog.Notify();
+ }
+}
+
+// Display an ongoing notification during the whole operation
+void FGitSourceControlMenu::DisplayInProgressNotification(const FText& InOperationInProgressString)
+{
+ if (!OperationInProgressNotification.IsValid())
+ {
+ FNotificationInfo Info(InOperationInProgressString);
+ Info.bFireAndForget = false;
+ Info.ExpireDuration = 0.0f;
+ Info.FadeOutDuration = 1.0f;
+ OperationInProgressNotification = FSlateNotificationManager::Get().AddNotification(Info);
+ if (OperationInProgressNotification.IsValid())
+ {
+ OperationInProgressNotification.Pin()->SetCompletionState(SNotificationItem::CS_Pending);
+ }
+ }
+}
+
+// Remove the ongoing notification at the end of the operation
+void FGitSourceControlMenu::RemoveInProgressNotification()
+{
+ if (OperationInProgressNotification.IsValid())
+ {
+ OperationInProgressNotification.Pin()->ExpireAndFadeout();
+ OperationInProgressNotification.Reset();
+ }
+}
+
+// Display a temporary success notification at the end of the operation
+void FGitSourceControlMenu::DisplaySucessNotification(const FName& InOperationName)
+{
+ const FText NotificationText = FText::Format(
+ LOCTEXT("SourceControlMenu_Success", "{0} operation was successful!"),
+ FText::FromName(InOperationName)
+ );
+ FNotificationInfo Info(NotificationText);
+ Info.bUseSuccessFailIcons = true;
+ Info.Image = FEditorStyle::GetBrush(TEXT("NotificationList.SuccessImage"));
+ FSlateNotificationManager::Get().AddNotification(Info);
+ UE_LOG(LogSourceControl, Log, TEXT("%s"), *NotificationText.ToString());
+}
+
+// Display a temporary failure notification at the end of the operation
+void FGitSourceControlMenu::DisplayFailureNotification(const FName& InOperationName)
+{
+ const FText NotificationText = FText::Format(
+ LOCTEXT("SourceControlMenu_Failure", "Error: {0} operation failed!"),
+ FText::FromName(InOperationName)
+ );
+ FNotificationInfo Info(NotificationText);
+ Info.ExpireDuration = 8.0f;
+ FSlateNotificationManager::Get().AddNotification(Info);
+ UE_LOG(LogSourceControl, Error, TEXT("%s"), *NotificationText.ToString());
+}
+
+void FGitSourceControlMenu::OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult)
+{
+ RemoveInProgressNotification();
+
+ if ((InOperation->GetName() == "Sync") || (InOperation->GetName() == "Revert"))
+ {
+ // Unstash any modifications if a stash was made at the beginning of the Sync operation
+ ReApplyStashedModifications();
+ // Reload packages that where unlinked at the beginning of the Sync/Revert operation
+ ReloadPackages(PackagesToReload);
+ }
+
+ // Report result with a notification
+ if (InResult == ECommandResult::Succeeded)
+ {
+ DisplaySucessNotification(InOperation->GetName());
+ }
+ else
+ {
+ DisplayFailureNotification(InOperation->GetName());
+ }
+}
+
+void FGitSourceControlMenu::AddMenuExtension(FMenuBuilder& Builder)
+{
+ Builder.AddMenuEntry(
+ LOCTEXT("GitPush", "Push"),
+ LOCTEXT("GitPushTooltip", "Push all local commits to the remote server."),
+ FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Submit"),
+ FUIAction(
+ FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::PushClicked),
+ FCanExecuteAction::CreateRaw(this, &FGitSourceControlMenu::HaveRemoteUrl)
+ )
+ );
+
+ Builder.AddMenuEntry(
+ LOCTEXT("GitSync", "Sync/Pull"),
+ LOCTEXT("GitSyncTooltip", "Update all files in the local repository to the latest version of the remote server."),
+ FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Sync"),
+ FUIAction(
+ FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::SyncClicked),
+ FCanExecuteAction::CreateRaw(this, &FGitSourceControlMenu::HaveRemoteUrl)
+ )
+ );
+
+ Builder.AddMenuEntry(
+ LOCTEXT("GitRevert", "Revert"),
+ LOCTEXT("GitRevertTooltip", "Revert all files in the repository to their unchanged state."),
+ FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Revert"),
+ FUIAction(
+ FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::RevertClicked),
+ FCanExecuteAction()
+ )
+ );
+
+ Builder.AddMenuEntry(
+ LOCTEXT("GitRefresh", "Refresh"),
+ LOCTEXT("GitRefreshTooltip", "Update the source control status of all files in the local repository."),
+ FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Refresh"),
+ FUIAction(
+ FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::RefreshClicked),
+ FCanExecuteAction()
+ )
+ );
+}
+
+TSharedRef FGitSourceControlMenu::OnExtendLevelEditorViewMenu(const TSharedRef CommandList)
+{
+ TSharedRef Extender(new FExtender());
+
+ Extender->AddMenuExtension(
+ "SourceControlActions",
+ EExtensionHook::After,
+ nullptr,
+ FMenuExtensionDelegate::CreateRaw(this, &FGitSourceControlMenu::AddMenuExtension));
+
+ return Extender;
+}
+
+#undef LOCTEXT_NAMESPACE
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlMenu.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlMenu.h
new file mode 100644
index 0000000..e7890d4
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlMenu.h
@@ -0,0 +1,61 @@
+// 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 "CoreMinimal.h"
+#include "ISourceControlProvider.h"
+
+class FToolBarBuilder;
+class FMenuBuilder;
+
+/** Git extension of the Source Control toolbar menu */
+class FGitSourceControlMenu
+{
+public:
+ void Register();
+ void Unregister();
+
+ /** This functions will be bound to appropriate Command. */
+ void PushClicked();
+ void SyncClicked();
+ void RevertClicked();
+ void RefreshClicked();
+
+private:
+ bool HaveRemoteUrl() const;
+
+ bool SaveDirtyPackages();
+ TArray ListAllPackages();
+ TArray UnlinkPackages(const TArray& InPackageNames);
+ void ReloadPackages(TArray& InPackagesToReload);
+
+ bool StashAwayAnyModifications();
+ void ReApplyStashedModifications();
+
+ void AddMenuExtension(FMenuBuilder& Builder);
+
+ TSharedRef OnExtendLevelEditorViewMenu(const TSharedRef CommandList);
+
+ void DisplayInProgressNotification(const FText& InOperationInProgressString);
+ void RemoveInProgressNotification();
+ void DisplaySucessNotification(const FName& InOperationName);
+ void DisplayFailureNotification(const FName& InOperationName);
+
+private:
+ FDelegateHandle ViewMenuExtenderHandle;
+
+ /** Was there a need to stash away modifications before Sync? */
+ bool bStashMadeBeforeSync;
+
+ /** Loaded packages to reload after a Sync or Revert operation */
+ TArray PackagesToReload;
+
+ /** Current source control operation from extended menu if any */
+ TWeakPtr OperationInProgressNotification;
+
+ /** Delegate called when a source control operation has completed */
+ void OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult);
+};
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlModule.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlModule.cpp
new file mode 100644
index 0000000..ce92181
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlModule.cpp
@@ -0,0 +1,65 @@
+// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com)
+//
+// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
+// or copy at http://opensource.org/licenses/MIT)
+
+#include "GitSourceControlModule.h"
+
+#include "Misc/App.h"
+#include "Modules/ModuleManager.h"
+#include "GitSourceControlOperations.h"
+#include "Features/IModularFeatures.h"
+
+#define LOCTEXT_NAMESPACE "GitSourceControl"
+
+template
+static TSharedRef CreateWorker()
+{
+ 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( "Push", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) );
+ GitSourceControlProvider.RegisterWorker( "CheckIn", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) );
+ GitSourceControlProvider.RegisterWorker( "Copy", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) );
+ GitSourceControlProvider.RegisterWorker( "Resolve", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) );
+
+ // load our settings
+ GitSourceControlSettings.LoadSettings();
+
+ // Bind our source control provider to the editor
+ IModularFeatures::Get().RegisterModularFeature( "SourceControl", &GitSourceControlProvider );
+}
+
+void FGitSourceControlModule::ShutdownModule()
+{
+ // shut down the provider, as this module is going away
+ GitSourceControlProvider.Close();
+
+ // unbind provider from editor
+ IModularFeatures::Get().UnregisterModularFeature("SourceControl", &GitSourceControlProvider);
+}
+
+void FGitSourceControlModule::SaveSettings()
+{
+ if (FApp::IsUnattended() || IsRunningCommandlet())
+ {
+ return;
+ }
+
+ GitSourceControlSettings.SaveSettings();
+}
+
+IMPLEMENT_MODULE(FGitSourceControlModule, GitSourceControl);
+
+#undef LOCTEXT_NAMESPACE
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlModule.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlModule.h
new file mode 100644
index 0000000..161e3db
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlModule.h
@@ -0,0 +1,114 @@
+// 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 "CoreMinimal.h"
+#include "Modules/ModuleInterface.h"
+#include "GitSourceControlSettings.h"
+#include "GitSourceControlProvider.h"
+
+/**
+
+UE4GitPlugin is a simple Git Source Control Plugin for Unreal Engine
+
+Written and contributed by Sebastien Rombauts (sebastien.rombauts@gmail.com)
+
+### Supported features
+- initialize a new Git local repository ('git init') to manage your UE4 Game Project
+ - can also create an appropriate .gitignore file as part of initialization
+ - can also create a .gitattributes file to enable Git LFS (Large File System) as part of initialization
+ - can also make the initial commit, with custom multi-line message
+ - can also configure the default remote origin URL
+- display status icons to show modified/added/deleted/untracked files
+- show history of a file
+- visual diff of a blueprint against depot or between previous versions of a file
+- revert modifications of a file
+- add, delete, rename a file
+- checkin/commit a file (cannot handle atomically more than 50 files)
+- migrate an asset between two projects if both are using Git
+- solve a merge conflict on a blueprint
+- show current branch name in status text
+- Sync to Pull (rebase) the current branch
+- Git LFS (Github, Gitlab, Bitbucket) is working with Git 2.10+ under Windows
+- Git LFS 2 File Locking is working with Git 2.10+ and Git LFS 2.0.0
+- Windows, Mac and Linux
+
+### TODO
+1. configure the name of the remote instead of default "origin"
+
+### TODO LFS 2.x File Locking
+
+Known issues:
+0. False error logs after a successful push:
+To https://github.com/SRombauts/UE4GitLfs2FileLocks.git
+ ee44ff5..59da15e HEAD -> master
+
+Use "TODO LFS" in the code to track things left to do/improve/refactor:
+1. IsUsingGitLfsLocking() should be cached in the Provider to avoid calling AccessSettings() too frequently
+ it can not change without re-initializing (at least re-connect) the Provider!
+2. Implement FGitSourceControlProvider::bWorkingOffline like the SubversionSourceControl plugin
+3. Trying to deactivate Git LFS 2 file locking afterward on the "Login to Source 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 ???)
+ - 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" si 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"
+4. Optimize usage of "git lfs locks", ie reduce the use of UdpateStatus() in Operations
+
+### What *cannot* be done presently
+- Branch/Merge are not in the current Editor workflow
+- Fetch is not in the current Editor workflow
+- Amend a commit is not in the current Editor workflow
+- Configure user name & email ('git config user.name' & git config user.email')
+
+### Known issues
+- the Editor does not show deleted files (only when deleted externally?)
+- the Editor does not show missing files
+- missing localization for git specific messages
+- displaying states of 'Engine' assets (also needs management of 'out of tree' files)
+- 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 source control settings */
+ FGitSourceControlSettings& AccessSettings()
+ {
+ return GitSourceControlSettings;
+ }
+ const FGitSourceControlSettings& AccessSettings() const
+ {
+ return GitSourceControlSettings;
+ }
+
+ /** Save the Git source control settings */
+ void SaveSettings();
+
+ /** Access the Git source control provider */
+ FGitSourceControlProvider& GetProvider()
+ {
+ return GitSourceControlProvider;
+ }
+ const FGitSourceControlProvider& GetProvider() const
+ {
+ return GitSourceControlProvider;
+ }
+
+private:
+ /** The Git source control provider */
+ FGitSourceControlProvider GitSourceControlProvider;
+
+ /** The settings for Git source control */
+ FGitSourceControlSettings GitSourceControlSettings;
+};
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlOperations.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlOperations.cpp
new file mode 100644
index 0000000..0c48986
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlOperations.cpp
@@ -0,0 +1,684 @@
+// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com)
+//
+// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
+// or copy at http://opensource.org/licenses/MIT)
+
+#include "GitSourceControlOperations.h"
+
+#include "Misc/Paths.h"
+#include "Modules/ModuleManager.h"
+#include "SourceControlOperations.h"
+#include "ISourceControlModule.h"
+#include "GitSourceControlModule.h"
+#include "GitSourceControlCommand.h"
+#include "GitSourceControlUtils.h"
+#include "Logging/MessageLog.h"
+
+#define LOCTEXT_NAMESPACE "GitSourceControl"
+
+FName FGitPush::GetName() const
+{
+ return "Push";
+}
+
+FText FGitPush::GetInProgressString() const
+{
+ // TODO Configure origin
+ return LOCTEXT("SourceControl_Push", "Pushing local commits to remote origin...");
+}
+
+
+FName FGitConnectWorker::GetName() const
+{
+ return "Connect";
+}
+
+bool FGitConnectWorker::Execute(FGitSourceControlCommand& InCommand)
+{
+ check(InCommand.Operation->GetName() == GetName());
+ TSharedRef Operation = StaticCastSharedRef(InCommand.Operation);
+
+ // Check Git Availability
+ if((InCommand.PathToGitBinary.Len() > 0) && GitSourceControlUtils::CheckGitAvailability(InCommand.PathToGitBinary))
+ {
+ // Now update the status of assets in Content/ directory and also Config files
+ TArray ProjectDirs;
+ ProjectDirs.Add(FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()));
+ ProjectDirs.Add(FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir()));
+ InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, ProjectDirs, InCommand.ErrorMessages, States);
+ if(!InCommand.bCommandSuccessful || InCommand.ErrorMessages.Num() > 0)
+ {
+ Operation->SetErrorText(LOCTEXT("NotAGitRepository", "Failed to enable Git source control. You need to initialize the project as a Git repository first."));
+ InCommand.bCommandSuccessful = false;
+ }
+ else
+ {
+ GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary);
+
+ if(InCommand.bUsingGitLfsLocking)
+ {
+ // Check server connection by checking lock status (when using Git LFS file Locking worflow)
+ InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("lfs locks"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), TArray(), InCommand.InfoMessages, InCommand.ErrorMessages);
+ }
+ }
+ }
+ else
+ {
+ Operation->SetErrorText(LOCTEXT("GitNotFound", "Failed to enable Git source control. You need to install Git and specify a valid path to git executable."));
+ InCommand.bCommandSuccessful = false;
+ }
+
+ return InCommand.bCommandSuccessful;
+}
+
+bool FGitConnectWorker::UpdateStates() const
+{
+ return GitSourceControlUtils::UpdateCachedStates(States);
+}
+
+FName FGitCheckOutWorker::GetName() const
+{
+ return "CheckOut";
+}
+
+bool FGitCheckOutWorker::Execute(FGitSourceControlCommand& InCommand)
+{
+ check(InCommand.Operation->GetName() == GetName());
+
+ if(InCommand.bUsingGitLfsLocking)
+ {
+ // lock files: execute the LFS command on relative filenames
+ InCommand.bCommandSuccessful = true;
+ const TArray RelativeFiles = GitSourceControlUtils::RelativeFilenames(InCommand.Files, InCommand.PathToRepositoryRoot);
+ for(const auto& RelativeFile : RelativeFiles)
+ {
+ TArray OneFile;
+ OneFile.Add(RelativeFile);
+ InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("lfs lock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), OneFile, InCommand.InfoMessages, InCommand.ErrorMessages);
+ }
+
+ // now update the status of our files
+ GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States);
+ }
+ else
+ {
+ InCommand.bCommandSuccessful = false;
+ }
+
+ return InCommand.bCommandSuccessful;
+}
+
+bool FGitCheckOutWorker::UpdateStates() const
+{
+ return GitSourceControlUtils::UpdateCachedStates(States);
+}
+
+static FText ParseCommitResults(const TArray& InResults)
+{
+ if(InResults.Num() >= 1)
+ {
+ const FString& FirstLine = InResults[0];
+ return FText::Format(LOCTEXT("CommitMessage", "Commited {0}."), FText::FromString(FirstLine));
+ }
+ return LOCTEXT("CommitMessageUnknown", "Submitted revision.");
+}
+
+// Get Locked Files (that is, CheckedOut files, not Added ones)
+const TArray GetLockedFiles(const TArray& InFiles)
+{
+ TArray LockedFiles;
+
+ FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl");
+ FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
+
+ TArray> LocalStates;
+ Provider.GetState(InFiles, LocalStates, EStateCacheUsage::Use);
+ for(const auto& State : LocalStates)
+ {
+ if(State->IsCheckedOut())
+ {
+ LockedFiles.Add(State->GetFilename());
+ }
+ }
+
+ return LockedFiles;
+}
+
+FName FGitCheckInWorker::GetName() const
+{
+ return "CheckIn";
+}
+
+bool FGitCheckInWorker::Execute(FGitSourceControlCommand& InCommand)
+{
+ check(InCommand.Operation->GetName() == GetName());
+
+ TSharedRef Operation = StaticCastSharedRef(InCommand.Operation);
+
+ // make a temp file to place our commit message in
+ FGitScopedTempFile CommitMsgFile(Operation->GetDescription());
+ if(CommitMsgFile.GetFilename().Len() > 0)
+ {
+ TArray Parameters;
+ FString ParamCommitMsgFilename = TEXT("--file=\"");
+ ParamCommitMsgFilename += FPaths::ConvertRelativePathToFull(CommitMsgFile.GetFilename());
+ ParamCommitMsgFilename += TEXT("\"");
+ Parameters.Add(ParamCommitMsgFilename);
+
+ InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommit(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters, InCommand.Files, InCommand.InfoMessages, InCommand.ErrorMessages);
+ if(InCommand.bCommandSuccessful)
+ {
+ // Remove any deleted files from status cache
+ FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl");
+ FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
+
+ TArray> LocalStates;
+ Provider.GetState(InCommand.Files, LocalStates, EStateCacheUsage::Use);
+ for(const auto& State : LocalStates)
+ {
+ if(State->IsDeleted())
+ {
+ Provider.RemoveFileFromCache(State->GetFilename());
+ }
+ }
+
+ Operation->SetSuccessMessage(ParseCommitResults(InCommand.InfoMessages));
+ const FString Message = (InCommand.InfoMessages.Num() > 0) ? InCommand.InfoMessages[0] : TEXT("");
+ UE_LOG(LogSourceControl, Log, TEXT("commit successful: %s"), *Message);
+
+ // git-lfs: push and unlock files
+ if(InCommand.bUsingGitLfsLocking && InCommand.bCommandSuccessful)
+ {
+ TArray Parameters2;
+ // TODO Configure origin
+ Parameters2.Add(TEXT("origin"));
+ Parameters2.Add(TEXT("HEAD"));
+ InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("push"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters2, TArray(), InCommand.InfoMessages, InCommand.ErrorMessages);
+ if(!InCommand.bCommandSuccessful)
+ {
+ // if out of date, pull first, then try again
+ bool bWasOutOfDate = false;
+ for (const auto& PushError : InCommand.ErrorMessages)
+ {
+ if (PushError.Contains(TEXT("[rejected]")) && PushError.Contains(TEXT("non-fast-forward")))
+ {
+ // Don't do it during iteration, want to append pull results to InCommand.ErrorMessages
+ bWasOutOfDate = true;
+ break;
+ }
+ }
+ if (bWasOutOfDate)
+ {
+ UE_LOG(LogSourceControl, Log, TEXT("Push failed because we're out of date, pulling automatically to try to resolve"));
+ // Use pull --rebase since that's what the pull command does by default
+ // This requires that we stash if dirty working copy though
+ bool bStashed = false;
+ bool bStashNeeded = false;
+ const TArray ParametersStatus{"--porcelain --untracked-files=no"};
+ TArray StatusInfoMessages;
+ TArray StatusErrorMessages;
+ // Check if there is any modification to the working tree
+ const bool bStatusOk = GitSourceControlUtils::RunCommand(TEXT("status"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, ParametersStatus, TArray(), StatusInfoMessages, StatusErrorMessages);
+ if ((bStatusOk) && (StatusInfoMessages.Num() > 0))
+ {
+ bStashNeeded = true;
+ const TArray ParametersStash{ "save \"Stashed by Unreal Engine Git Plugin\"" };
+ bStashed = GitSourceControlUtils::RunCommand(TEXT("stash"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, ParametersStash, TArray(), InCommand.InfoMessages, InCommand.ErrorMessages);
+ if (!bStashed)
+ {
+ FMessageLog SourceControlLog("SourceControl");
+ SourceControlLog.Warning(LOCTEXT("SourceControlMenu_StashFailed", "Stashing away modifications failed!"));
+ SourceControlLog.Notify();
+ }
+ }
+ if (!bStashNeeded || bStashed)
+ {
+ InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("pull --rebase"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), TArray(), InCommand.InfoMessages, InCommand.ErrorMessages);
+ if (InCommand.bCommandSuccessful)
+ {
+ // Repeat the push
+ InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("push origin HEAD"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), TArray(), InCommand.InfoMessages, InCommand.ErrorMessages);
+ }
+
+ // Succeed or fail, restore the stash
+ if (bStashed)
+ {
+ const TArray ParametersStashPop{ "pop" };
+ InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("stash"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, ParametersStashPop, TArray(), InCommand.InfoMessages, InCommand.ErrorMessages);
+ if (!InCommand.bCommandSuccessful)
+ {
+ FMessageLog SourceControlLog("SourceControl");
+ SourceControlLog.Warning(LOCTEXT("SourceControlMenu_UnstashFailed", "Unstashing previously saved modifications failed!"));
+ SourceControlLog.Notify();
+ }
+ }
+ }
+ }
+ }
+ if(InCommand.bCommandSuccessful)
+ {
+ // unlock files: execute the LFS command on relative filenames
+ // (unlock only locked files, that is, not Added files)
+ const TArray LockedFiles = GetLockedFiles(InCommand.Files);
+ if(LockedFiles.Num() > 0)
+ {
+ const TArray RelativeFiles = GitSourceControlUtils::RelativeFilenames(LockedFiles, InCommand.PathToRepositoryRoot);
+ for(const auto& RelativeFile : RelativeFiles)
+ {
+ TArray OneFile;
+ OneFile.Add(RelativeFile);
+ GitSourceControlUtils::RunCommand(TEXT("lfs unlock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), OneFile, InCommand.InfoMessages, InCommand.ErrorMessages);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // now update the status of our files
+ GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States);
+ GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary);
+
+ return InCommand.bCommandSuccessful;
+}
+
+bool FGitCheckInWorker::UpdateStates() const
+{
+ return GitSourceControlUtils::UpdateCachedStates(States);
+}
+
+FName FGitMarkForAddWorker::GetName() const
+{
+ return "MarkForAdd";
+}
+
+bool FGitMarkForAddWorker::Execute(FGitSourceControlCommand& InCommand)
+{
+ check(InCommand.Operation->GetName() == GetName());
+
+ InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), InCommand.Files, InCommand.InfoMessages, InCommand.ErrorMessages);
+
+ // now update the status of our files
+ GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States);
+
+ return InCommand.bCommandSuccessful;
+}
+
+bool FGitMarkForAddWorker::UpdateStates() const
+{
+ return GitSourceControlUtils::UpdateCachedStates(States);
+}
+
+FName FGitDeleteWorker::GetName() const
+{
+ return "Delete";
+}
+
+bool FGitDeleteWorker::Execute(FGitSourceControlCommand& InCommand)
+{
+ check(InCommand.Operation->GetName() == GetName());
+
+ InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("rm"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), InCommand.Files, InCommand.InfoMessages, InCommand.ErrorMessages);
+
+ // now update the status of our files
+ GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States);
+
+ return InCommand.bCommandSuccessful;
+}
+
+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)
+{
+ FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl");
+ FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
+
+ const TArray Files = (InFiles.Num() > 0) ? (InFiles) : (Provider.GetFilesInCache());
+
+ TArray> LocalStates;
+ Provider.GetState(Files, LocalStates, EStateCacheUsage::Use);
+ for(const auto& State : LocalStates)
+ {
+ if(FPaths::FileExists(State->GetFilename()))
+ {
+ if(State->IsAdded())
+ {
+ OutAllExistingFiles.Add(State->GetFilename());
+ }
+ else if(State->IsModified())
+ {
+ OutOtherThanAddedExistingFiles.Add(State->GetFilename());
+ OutAllExistingFiles.Add(State->GetFilename());
+ }
+ else if(State->CanRevert()) // for locked but unmodified files
+ {
+ OutOtherThanAddedExistingFiles.Add(State->GetFilename());
+ }
+ }
+ else
+ {
+ if (State->IsSourceControlled())
+ {
+ OutMissingFiles.Add(State->GetFilename());
+ }
+ }
+ }
+}
+
+FName FGitRevertWorker::GetName() const
+{
+ return "Revert";
+}
+
+bool FGitRevertWorker::Execute(FGitSourceControlCommand& InCommand)
+{
+ // Filter files by status to use the right "revert" commands on them
+ TArray MissingFiles;
+ TArray AllExistingFiles;
+ TArray OtherThanAddedExistingFiles;
+ GetMissingVsExistingFiles(InCommand.Files, MissingFiles, AllExistingFiles, OtherThanAddedExistingFiles);
+
+ InCommand.bCommandSuccessful = true;
+ if(MissingFiles.Num() > 0)
+ {
+ // "Added" files that have been deleted needs to be removed from source control
+ InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("rm"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), MissingFiles, InCommand.InfoMessages, InCommand.ErrorMessages);
+ }
+ if(AllExistingFiles.Num() > 0)
+ {
+ // reset any changes already added to the index
+ InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("reset"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), AllExistingFiles, InCommand.InfoMessages, InCommand.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")
+ InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("checkout"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), OtherThanAddedExistingFiles, InCommand.InfoMessages, InCommand.ErrorMessages);
+ }
+
+ if(InCommand.bUsingGitLfsLocking)
+ {
+ // unlock files: execute the LFS command on relative filenames
+ // (unlock only locked files, that is, not Added files)
+ const TArray LockedFiles = GetLockedFiles(OtherThanAddedExistingFiles);
+ if(LockedFiles.Num() > 0)
+ {
+ const TArray RelativeFiles = GitSourceControlUtils::RelativeFilenames(LockedFiles, InCommand.PathToRepositoryRoot);
+ for(const auto& RelativeFile : RelativeFiles)
+ {
+ TArray OneFile;
+ OneFile.Add(RelativeFile);
+ GitSourceControlUtils::RunCommand(TEXT("lfs unlock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), OneFile, InCommand.InfoMessages, InCommand.ErrorMessages);
+ }
+ }
+ }
+
+ // 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);
+ }
+
+ // now update the status of our files
+ GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, FilesToUpdate, InCommand.ErrorMessages, States);
+
+ return InCommand.bCommandSuccessful;
+}
+
+bool FGitRevertWorker::UpdateStates() const
+{
+ return GitSourceControlUtils::UpdateCachedStates(States);
+}
+
+FName FGitSyncWorker::GetName() const
+{
+ return "Sync";
+}
+
+bool FGitSyncWorker::Execute(FGitSourceControlCommand& InCommand)
+{
+ // pull the branch to get remote changes by rebasing any local commits (not merging them to avoid complex graphs)
+ TArray Parameters;
+ Parameters.Add(TEXT("--rebase"));
+ Parameters.Add(TEXT("--autostash"));
+ // TODO Configure origin
+ Parameters.Add(TEXT("origin"));
+ Parameters.Add(TEXT("HEAD"));
+ InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("pull"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters, TArray(), InCommand.InfoMessages, InCommand.ErrorMessages);
+
+ // now update the status of our files
+ GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States);
+ GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary);
+
+ return InCommand.bCommandSuccessful;
+}
+
+bool FGitSyncWorker::UpdateStates() const
+{
+ return GitSourceControlUtils::UpdateCachedStates(States);
+}
+
+
+FName FGitPushWorker::GetName() const
+{
+ return "Push";
+}
+
+bool FGitPushWorker::Execute(FGitSourceControlCommand& InCommand)
+{
+
+ // If we have any locked files, check if we should unlock them
+ TArray FilesToUnlock;
+ if (InCommand.bUsingGitLfsLocking)
+ {
+ TMap Locks;
+ // Get locks as relative paths
+ GitSourceControlUtils::GetAllLocks(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, false, InCommand.ErrorMessages, Locks);
+ if(Locks.Num() > 0)
+ {
+ // test to see what lfs files we would push, and compare to locked files, unlock after if push OK
+ FString BranchName;
+ GitSourceControlUtils::GetBranchName(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, BranchName);
+
+ TArray LfsPushParameters;
+ LfsPushParameters.Add(TEXT("push"));
+ LfsPushParameters.Add(TEXT("--dry-run"));
+ LfsPushParameters.Add(TEXT("origin"));
+ LfsPushParameters.Add(BranchName);
+ TArray LfsPushInfoMessages;
+ TArray LfsPushErrMessages;
+ InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("lfs"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, LfsPushParameters, TArray(), LfsPushInfoMessages, LfsPushErrMessages);
+
+ if(InCommand.bCommandSuccessful)
+ {
+ // Result format is of the form
+ // push f4ee401c063058a78842bb3ed98088e983c32aa447f346db54fa76f844a7e85e => Path/To/Asset.uasset
+ // With some potential informationals we can ignore
+ for (auto& Line : LfsPushInfoMessages)
+ {
+ if (Line.StartsWith(TEXT("push")))
+ {
+ FString Prefix, Filename;
+ if (Line.Split(TEXT("=>"), &Prefix, &Filename))
+ {
+ Filename = Filename.TrimStartAndEnd();
+ if (Locks.Contains(Filename))
+ {
+ // We do not need to check user or if the file has local modifications before attempting unlocking, git-lfs will reject the unlock if so
+ // No point duplicating effort here
+ FilesToUnlock.Add(Filename);
+ UE_LOG(LogSourceControl, Log, TEXT("Post-push will try to unlock: %s"), *Filename);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ }
+ // push the branch to its default remote
+ // (works only if the default remote "origin" is set and does not require authentication)
+ TArray Parameters;
+ Parameters.Add(TEXT("--set-upstream"));
+ // TODO Configure origin
+ Parameters.Add(TEXT("origin"));
+ Parameters.Add(TEXT("HEAD"));
+ InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("push"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters, TArray(), InCommand.InfoMessages, InCommand.ErrorMessages);
+
+ if(InCommand.bCommandSuccessful && InCommand.bUsingGitLfsLocking && FilesToUnlock.Num() > 0)
+ {
+ // unlock files: execute the LFS command on relative filenames
+ for(const auto& FileToUnlock : FilesToUnlock)
+ {
+ TArray OneFile;
+ OneFile.Add(FileToUnlock);
+ bool bUnlocked = GitSourceControlUtils::RunCommand(TEXT("lfs unlock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), OneFile, InCommand.InfoMessages, InCommand.ErrorMessages);
+ if (!bUnlocked)
+ {
+ // Report but don't fail, it's not essential
+ UE_LOG(LogSourceControl, Log, TEXT("Unlock failed for %s"), *FileToUnlock);
+ }
+ }
+
+ // We need to update status if we unlock
+ // This command needs absolute filenames
+ TArray AbsFilesToUnlock = GitSourceControlUtils::AbsoluteFilenames(FilesToUnlock, InCommand.PathToRepositoryRoot);
+ GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, AbsFilesToUnlock, InCommand.ErrorMessages, States);
+
+ }
+
+ return InCommand.bCommandSuccessful;
+}
+
+bool FGitPushWorker::UpdateStates() const
+{
+ return GitSourceControlUtils::UpdateCachedStates(States);
+}
+
+FName FGitUpdateStatusWorker::GetName() const
+{
+ return "UpdateStatus";
+}
+
+bool FGitUpdateStatusWorker::Execute(FGitSourceControlCommand& InCommand)
+{
+ check(InCommand.Operation->GetName() == GetName());
+
+ TSharedRef Operation = StaticCastSharedRef(InCommand.Operation);
+
+ if(InCommand.Files.Num() > 0)
+ {
+ InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States);
+ GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
+
+ if(Operation->ShouldUpdateHistory())
+ {
+ for(int32 Index = 0; Index < States.Num(); Index++)
+ {
+ FString& File = InCommand.Files[Index];
+ TGitSourceControlHistory History;
+
+ if(States[Index].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.ErrorMessages, History);
+ }
+ // Get the history of the file in the current branch
+ InCommand.bCommandSuccessful &= GitSourceControlUtils::RunGetHistory(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, File, false, InCommand.ErrorMessages, History);
+ Histories.Add(*File, History);
+ }
+ }
+ }
+ else
+ {
+ // no path provided: only update the status of assets in Content/ directory and also Config files
+ TArray ProjectDirs;
+ ProjectDirs.Add(FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()));
+ ProjectDirs.Add(FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir()));
+ InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, ProjectDirs, InCommand.ErrorMessages, States);
+ }
+
+ 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)
+
+ return InCommand.bCommandSuccessful;
+}
+
+bool FGitUpdateStatusWorker::UpdateStates() const
+{
+ bool bUpdated = GitSourceControlUtils::UpdateCachedStates(States);
+
+ FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked( "GitSourceControl" );
+ FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
+
+ const FDateTime Now = FDateTime::Now();
+
+ // add history, if any
+ for(const auto& History : Histories)
+ {
+ TSharedRef State = Provider.GetStateInternal(History.Key);
+ State->History = History.Value;
+ State->TimeStamp = Now;
+ bUpdated = true;
+ }
+
+ return bUpdated;
+}
+
+FName FGitCopyWorker::GetName() const
+{
+ return "Copy";
+}
+
+bool FGitCopyWorker::Execute(FGitSourceControlCommand& InCommand)
+{
+ check(InCommand.Operation->GetName() == GetName());
+
+ // 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 commited 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, TArray(), InCommand.Files, InCommand.InfoMessages, InCommand.ErrorMessages);
+
+ return InCommand.bCommandSuccessful;
+}
+
+bool FGitCopyWorker::UpdateStates() const
+{
+ return GitSourceControlUtils::UpdateCachedStates(States);
+}
+
+FName FGitResolveWorker::GetName() const
+{
+ return "Resolve";
+}
+
+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, TArray(), InCommand.Files, Results, InCommand.ErrorMessages);
+
+ // now update the status of our files
+ GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States);
+
+ return InCommand.bCommandSuccessful;
+}
+
+bool FGitResolveWorker::UpdateStates() const
+{
+ return GitSourceControlUtils::UpdateCachedStates(States);
+}
+
+#undef LOCTEXT_NAMESPACE
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlOperations.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlOperations.h
new file mode 100644
index 0000000..5b9fb81
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlOperations.h
@@ -0,0 +1,192 @@
+// 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 "CoreMinimal.h"
+#include "IGitSourceControlWorker.h"
+#include "GitSourceControlState.h"
+
+#include "ISourceControlOperation.h"
+
+/**
+ * Internal operation used to push local commits to configured remote origin
+*/
+class FGitPush : public ISourceControlOperation
+{
+public:
+ // ISourceControlOperation interface
+ virtual FName GetName() const override;
+
+ virtual FText GetInProgressString() const override;
+};
+
+/** Called when first activated on a project, and then at project load time.
+ * Look for the root directory of the git repository (where the ".git/" subdirectory is located). */
+class FGitConnectWorker : public IGitSourceControlWorker
+{
+public:
+ virtual ~FGitConnectWorker() {}
+ // IGitSourceControlWorker interface
+ virtual FName GetName() const override;
+ virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
+ virtual bool UpdateStates() const override;
+
+public:
+ /** Temporary states for results */
+ TArray States;
+};
+
+/** Lock (check-out) a set of files using Git LFS 2. */
+class FGitCheckOutWorker : public IGitSourceControlWorker
+{
+public:
+ virtual ~FGitCheckOutWorker() {}
+ // IGitSourceControlWorker interface
+ virtual FName GetName() const override;
+ virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
+ virtual bool UpdateStates() const override;
+
+public:
+ /** Temporary states for results */
+ TArray States;
+};
+
+/** Commit (check-in) a set of files to the local depot. */
+class FGitCheckInWorker : public IGitSourceControlWorker
+{
+public:
+ virtual ~FGitCheckInWorker() {}
+ // IGitSourceControlWorker interface
+ virtual FName GetName() const override;
+ virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
+ virtual bool UpdateStates() const override;
+
+public:
+ /** Temporary states for results */
+ TArray States;
+};
+
+/** Add an untraked file to source control (so only a subset of the git add command). */
+class FGitMarkForAddWorker : public IGitSourceControlWorker
+{
+public:
+ virtual ~FGitMarkForAddWorker() {}
+ // IGitSourceControlWorker interface
+ virtual FName GetName() const override;
+ virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
+ virtual bool UpdateStates() const override;
+
+public:
+ /** Temporary states for results */
+ TArray States;
+};
+
+/** Delete a file and remove it from source control. */
+class FGitDeleteWorker : public IGitSourceControlWorker
+{
+public:
+ virtual ~FGitDeleteWorker() {}
+ // IGitSourceControlWorker interface
+ virtual FName GetName() const override;
+ virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
+ virtual bool UpdateStates() const override;
+
+public:
+ /** Temporary states for results */
+ TArray States;
+};
+
+/** Revert any change to a file to its state on the local depot. */
+class FGitRevertWorker : public IGitSourceControlWorker
+{
+public:
+ virtual ~FGitRevertWorker() {}
+ // IGitSourceControlWorker interface
+ virtual FName GetName() const override;
+ virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
+ virtual bool UpdateStates() const override;
+
+public:
+ /** Temporary states for results */
+ TArray States;
+};
+
+/** Git pull --rebase to update branch from its configured remote */
+class FGitSyncWorker : public IGitSourceControlWorker
+{
+public:
+ virtual ~FGitSyncWorker() {}
+ // IGitSourceControlWorker interface
+ virtual FName GetName() const override;
+ virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
+ virtual bool UpdateStates() const override;
+
+public:
+ /** Temporary states for results */
+ TArray States;
+};
+
+/** Git push to publish branch for its configured remote */
+class FGitPushWorker : public IGitSourceControlWorker
+{
+public:
+ virtual ~FGitPushWorker() {}
+ // IGitSourceControlWorker interface
+ virtual FName GetName() const override;
+ virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
+ virtual bool UpdateStates() const override;
+
+public:
+ /** Temporary states for results */
+ TArray States;
+};
+
+/** Get source control status of files on local working copy. */
+class FGitUpdateStatusWorker : public IGitSourceControlWorker
+{
+public:
+ virtual ~FGitUpdateStatusWorker() {}
+ // IGitSourceControlWorker interface
+ virtual FName GetName() const override;
+ virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
+ virtual bool UpdateStates() const override;
+
+public:
+ /** Temporary states for results */
+ TArray States;
+
+ /** Map of filenames to history */
+ TMap Histories;
+};
+
+/** Copy or Move operation on a single file */
+class FGitCopyWorker : public IGitSourceControlWorker
+{
+public:
+ virtual ~FGitCopyWorker() {}
+ // IGitSourceControlWorker interface
+ virtual FName GetName() const override;
+ virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
+ virtual bool UpdateStates() const override;
+
+public:
+ /** Temporary states for results */
+ TArray States;
+};
+
+/** git add to mark a conflict as resolved */
+class FGitResolveWorker : public IGitSourceControlWorker
+{
+public:
+ virtual ~FGitResolveWorker() {}
+ virtual FName GetName() const override;
+ virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
+ virtual bool UpdateStates() const override;
+
+private:
+ /** Temporary states for results */
+ TArray States;
+};
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlPrivatePCH.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlPrivatePCH.h
new file mode 100644
index 0000000..e5d9454
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlPrivatePCH.h
@@ -0,0 +1,15 @@
+// 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 "CoreMinimal.h"
+#include "ISourceControlModule.h"
+#include "ISourceControlOperation.h"
+#include "ISourceControlProvider.h"
+#include "ISourceControlRevision.h"
+#include "ISourceControlState.h"
+#include "Misc/Paths.h"
+#include "Modules/ModuleManager.h"
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlProvider.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlProvider.cpp
new file mode 100644
index 0000000..aa6c256
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlProvider.cpp
@@ -0,0 +1,467 @@
+// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com)
+//
+// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
+// or copy at http://opensource.org/licenses/MIT)
+
+#include "GitSourceControlProvider.h"
+
+#include "HAL/PlatformProcess.h"
+#include "Misc/Paths.h"
+#include "Misc/QueuedThreadPool.h"
+#include "Modules/ModuleManager.h"
+#include "Widgets/DeclarativeSyntaxSupport.h"
+#include "GitSourceControlCommand.h"
+#include "ISourceControlModule.h"
+#include "GitSourceControlModule.h"
+#include "GitSourceControlUtils.h"
+#include "SGitSourceControlSettings.h"
+#include "Logging/MessageLog.h"
+#include "ScopedSourceControlProgress.h"
+#include "SourceControlHelpers.h"
+#include "SourceControlOperations.h"
+#include "Interfaces/IPluginManager.h"
+
+#define LOCTEXT_NAMESPACE "GitSourceControl"
+
+static FName ProviderName("Git LFS 2");
+
+void FGitSourceControlProvider::Init(bool bForceConnection)
+{
+ // Init() is called multiple times at startup: do not check git each time
+ if(!bGitAvailable)
+ {
+ const TSharedPtr Plugin = IPluginManager::Get().FindPlugin(TEXT("GitSourceControl"));
+ if(Plugin.IsValid())
+ {
+ UE_LOG(LogSourceControl, Log, TEXT("Git plugin '%s'"), *(Plugin->GetDescriptor().VersionName));
+ }
+
+ CheckGitAvailability();
+
+ const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl");
+ bUsingGitLfsLocking = GitSourceControl.AccessSettings().IsUsingGitLfsLocking();
+ }
+
+ // bForceConnection: not used anymore
+}
+
+void FGitSourceControlProvider::CheckGitAvailability()
+{
+ FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl");
+ FString PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
+ if(PathToGitBinary.IsEmpty())
+ {
+ // Try to find Git binary, and update settings accordingly
+ PathToGitBinary = GitSourceControlUtils::FindGitBinaryPath();
+ if(!PathToGitBinary.IsEmpty())
+ {
+ GitSourceControl.AccessSettings().SetBinaryPath(PathToGitBinary);
+ }
+ }
+
+ if(!PathToGitBinary.IsEmpty())
+ {
+ UE_LOG(LogSourceControl, Log, TEXT("Using '%s'"), *PathToGitBinary);
+ bGitAvailable = GitSourceControlUtils::CheckGitAvailability(PathToGitBinary, &GitVersion);
+ if(bGitAvailable)
+ {
+ CheckRepositoryStatus(PathToGitBinary);
+ }
+ }
+ else
+ {
+ bGitAvailable = false;
+ }
+}
+
+void FGitSourceControlProvider::CheckRepositoryStatus(const FString& InPathToGitBinary)
+{
+ // Find the path to the root Git directory (if any, else uses the ProjectDir)
+ const FString PathToProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
+ bGitRepositoryFound = GitSourceControlUtils::FindRootDirectory(PathToProjectDir, PathToRepositoryRoot);
+ if(bGitRepositoryFound)
+ {
+ GitSourceControlMenu.Register();
+
+ // Get branch name
+ bGitRepositoryFound = GitSourceControlUtils::GetBranchName(InPathToGitBinary, PathToRepositoryRoot, BranchName);
+ if(bGitRepositoryFound)
+ {
+ GitSourceControlUtils::GetRemoteUrl(InPathToGitBinary, PathToRepositoryRoot, RemoteUrl);
+ }
+ else
+ {
+ UE_LOG(LogSourceControl, Error, TEXT("'%s' is not a valid Git repository"), *PathToRepositoryRoot);
+ }
+ }
+ else
+ {
+ UE_LOG(LogSourceControl, Warning, TEXT("'%s' is not part of a Git repository"), *FPaths::ProjectDir());
+ }
+
+ // Get user name & email (of the repository, else from the global Git config)
+ GitSourceControlUtils::GetUserConfig(InPathToGitBinary, PathToRepositoryRoot, UserName, UserEmail);
+}
+
+void FGitSourceControlProvider::Close()
+{
+ // clear the cache
+ StateCache.Empty();
+ // Remove all extensions to the "Source Control" menu in the Editor Toolbar
+ GitSourceControlMenu.Unregister();
+
+ bGitAvailable = false;
+ bGitRepositoryFound = false;
+ UserName.Empty();
+ UserEmail.Empty();
+}
+
+TSharedRef FGitSourceControlProvider::GetStateInternal(const FString& Filename)
+{
+ TSharedRef* State = StateCache.Find(Filename);
+ if(State != NULL)
+ {
+ // found cached item
+ return (*State);
+ }
+ else
+ {
+ // cache an unknown state for this item
+ TSharedRef NewState = MakeShareable( new FGitSourceControlState(Filename, bUsingGitLfsLocking) );
+ StateCache.Add(Filename, NewState);
+ return NewState;
+ }
+}
+
+FText FGitSourceControlProvider::GetStatusText() const
+{
+ FFormatNamedArguments Args;
+ Args.Add( TEXT("RepositoryName"), FText::FromString(PathToRepositoryRoot) );
+ Args.Add( TEXT("RemoteUrl"), FText::FromString(RemoteUrl) );
+ Args.Add( TEXT("UserName"), FText::FromString(UserName) );
+ Args.Add( TEXT("UserEmail"), FText::FromString(UserEmail) );
+ Args.Add( TEXT("BranchName"), FText::FromString(BranchName) );
+ Args.Add( TEXT("CommitId"), FText::FromString(CommitId.Left(8)) );
+ Args.Add( TEXT("CommitSummary"), FText::FromString(CommitSummary) );
+
+ return FText::Format( NSLOCTEXT("Status", "Provider: Git\nEnabledLabel", "Local repository: {RepositoryName}\nRemote origin: {RemoteUrl}\nUser: {UserName}\nE-mail: {UserEmail}\n[{BranchName} {CommitId}] {CommitSummary}"), Args );
+}
+
+/** Quick check if source control is enabled */
+bool FGitSourceControlProvider::IsEnabled() const
+{
+ return bGitRepositoryFound;
+}
+
+/** Quick check if source control is available for use (useful for server-based providers) */
+bool FGitSourceControlProvider::IsAvailable() const
+{
+ return bGitRepositoryFound;
+}
+
+const FName& FGitSourceControlProvider::GetName(void) const
+{
+ return ProviderName;
+}
+
+ECommandResult::Type FGitSourceControlProvider::GetState( const TArray& InFiles, TArray< TSharedRef >& OutState, EStateCacheUsage::Type InStateCacheUsage )
+{
+ if(!IsEnabled())
+ {
+ return ECommandResult::Failed;
+ }
+
+ TArray AbsoluteFiles = SourceControlHelpers::AbsoluteFilenames(InFiles);
+
+ if(InStateCacheUsage == EStateCacheUsage::ForceUpdate)
+ {
+ Execute(ISourceControlOperation::Create(), AbsoluteFiles);
+ }
+
+ for(const auto& AbsoluteFile : AbsoluteFiles)
+ {
+ OutState.Add(GetStateInternal(*AbsoluteFile));
+ }
+
+ return ECommandResult::Succeeded;
+}
+
+TArray FGitSourceControlProvider::GetCachedStateByPredicate(TFunctionRef Predicate) const
+{
+ TArray Result;
+ for(const auto& CacheItem : StateCache)
+ {
+ FSourceControlStateRef State = CacheItem.Value;
+ if(Predicate(State))
+ {
+ Result.Add(State);
+ }
+ }
+ return Result;
+}
+
+bool FGitSourceControlProvider::RemoveFileFromCache(const FString& Filename)
+{
+ return StateCache.Remove(Filename) > 0;
+}
+
+/** Get files in cache */
+TArray FGitSourceControlProvider::GetFilesInCache()
+{
+ TArray Files;
+ for (const auto& State : StateCache)
+ {
+ Files.Add(State.Key);
+ }
+ return Files;
+}
+
+FDelegateHandle FGitSourceControlProvider::RegisterSourceControlStateChanged_Handle( const FSourceControlStateChanged::FDelegate& SourceControlStateChanged )
+{
+ return OnSourceControlStateChanged.Add( SourceControlStateChanged );
+}
+
+void FGitSourceControlProvider::UnregisterSourceControlStateChanged_Handle( FDelegateHandle Handle )
+{
+ OnSourceControlStateChanged.Remove( Handle );
+}
+
+ECommandResult::Type FGitSourceControlProvider::Execute( const TSharedRef& InOperation, const TArray& InFiles, EConcurrency::Type InConcurrency, const FSourceControlOperationComplete& InOperationCompleteDelegate )
+{
+ if(!IsEnabled() && !(InOperation->GetName() == "Connect")) // Only Connect operation allowed while not Enabled (Repository found)
+ {
+ InOperationCompleteDelegate.ExecuteIfBound(InOperation, ECommandResult::Failed);
+ return ECommandResult::Failed;
+ }
+
+ TArray AbsoluteFiles = SourceControlHelpers::AbsoluteFilenames(InFiles);
+
+ // Query to see if we allow this operation
+ TSharedPtr Worker = CreateWorker(InOperation->GetName());
+ if(!Worker.IsValid())
+ {
+ // this operation is unsupported by this source control provider
+ FFormatNamedArguments Arguments;
+ Arguments.Add( TEXT("OperationName"), FText::FromName(InOperation->GetName()) );
+ Arguments.Add( TEXT("ProviderName"), FText::FromName(GetName()) );
+ FText Message(FText::Format(LOCTEXT("UnsupportedOperation", "Operation '{OperationName}' not supported by source control provider '{ProviderName}'"), Arguments));
+ FMessageLog("SourceControl").Error(Message);
+ InOperation->AddErrorMessge(Message);
+
+ InOperationCompleteDelegate.ExecuteIfBound(InOperation, ECommandResult::Failed);
+ return ECommandResult::Failed;
+ }
+
+ FGitSourceControlCommand* Command = new FGitSourceControlCommand(InOperation, Worker.ToSharedRef());
+ Command->Files = AbsoluteFiles;
+ Command->OperationCompleteDelegate = InOperationCompleteDelegate;
+
+ // fire off operation
+ if(InConcurrency == EConcurrency::Synchronous)
+ {
+ Command->bAutoDelete = false;
+
+ UE_LOG(LogSourceControl, Log, TEXT("ExecuteSynchronousCommand(%s)"), *InOperation->GetName().ToString());
+ return ExecuteSynchronousCommand(*Command, InOperation->GetInProgressString());
+ }
+ else
+ {
+ Command->bAutoDelete = true;
+
+ UE_LOG(LogSourceControl, Log, TEXT("IssueAsynchronousCommand(%s)"), *InOperation->GetName().ToString());
+ return IssueCommand(*Command);
+ }
+}
+
+bool FGitSourceControlProvider::CanCancelOperation( const TSharedRef& InOperation ) const
+{
+ return false;
+}
+
+void FGitSourceControlProvider::CancelOperation( const TSharedRef& InOperation )
+{
+}
+
+bool FGitSourceControlProvider::UsesLocalReadOnlyState() const
+{
+ return bUsingGitLfsLocking; // Git LFS Lock uses read-only state
+}
+
+bool FGitSourceControlProvider::UsesChangelists() const
+{
+ return false;
+}
+
+bool FGitSourceControlProvider::UsesCheckout() const
+{
+ return bUsingGitLfsLocking; // Git LFS Lock uses read-only state
+}
+
+TSharedPtr FGitSourceControlProvider::CreateWorker(const FName& InOperationName) const
+{
+ const FGetGitSourceControlWorker* Operation = WorkersMap.Find(InOperationName);
+ if(Operation != nullptr)
+ {
+ return Operation->Execute();
+ }
+
+ return nullptr;
+}
+
+void FGitSourceControlProvider::RegisterWorker( const FName& InName, const FGetGitSourceControlWorker& InDelegate )
+{
+ WorkersMap.Add( InName, InDelegate );
+}
+
+void FGitSourceControlProvider::OutputCommandMessages(const FGitSourceControlCommand& InCommand) const
+{
+ FMessageLog SourceControlLog("SourceControl");
+
+ for(int32 ErrorIndex = 0; ErrorIndex < InCommand.ErrorMessages.Num(); ++ErrorIndex)
+ {
+ SourceControlLog.Error(FText::FromString(InCommand.ErrorMessages[ErrorIndex]));
+ }
+
+ for(int32 InfoIndex = 0; InfoIndex < InCommand.InfoMessages.Num(); ++InfoIndex)
+ {
+ SourceControlLog.Info(FText::FromString(InCommand.InfoMessages[InfoIndex]));
+ }
+}
+
+void FGitSourceControlProvider::UpdateRepositoryStatus(const class FGitSourceControlCommand& InCommand)
+{
+ // For all operations running UpdateStatus, get Commit informations:
+ if (!InCommand.CommitId.IsEmpty())
+ {
+ CommitId = InCommand.CommitId;
+ CommitSummary = InCommand.CommitSummary;
+ }
+}
+
+void FGitSourceControlProvider::Tick()
+{
+ bool bStatesUpdated = false;
+
+ for(int32 CommandIndex = 0; CommandIndex < CommandQueue.Num(); ++CommandIndex)
+ {
+ FGitSourceControlCommand& Command = *CommandQueue[CommandIndex];
+ if(Command.bExecuteProcessed)
+ {
+ // Remove command from the queue
+ CommandQueue.RemoveAt(CommandIndex);
+
+ // Update respository status on UpdateStatus operations
+ UpdateRepositoryStatus(Command);
+
+ // let command update the states of any files
+ bStatesUpdated |= Command.Worker->UpdateStates();
+
+ // dump any messages to output log
+ OutputCommandMessages(Command);
+
+ // run the completion delegate callback if we have one bound
+ Command.ReturnResults();
+
+ // commands that are left in the array during a tick need to be deleted
+ if(Command.bAutoDelete)
+ {
+ // Only delete commands that are not running 'synchronously'
+ delete &Command;
+ }
+
+ // only do one command per tick loop, as we dont want concurrent modification
+ // of the command queue (which can happen in the completion delegate)
+ break;
+ }
+ }
+
+ if(bStatesUpdated)
+ {
+ OnSourceControlStateChanged.Broadcast();
+ }
+}
+
+TArray< TSharedRef > FGitSourceControlProvider::GetLabels( const FString& InMatchingSpec ) const
+{
+ TArray< TSharedRef > Tags;
+
+ // NOTE list labels. Called by CrashDebugHelper() (to remote debug Engine crash)
+ // and by SourceControlHelpers::AnnotateFile() (to add source file to report)
+ // Reserved for internal use by Epic Games with Perforce only
+ return Tags;
+}
+
+#if SOURCE_CONTROL_WITH_SLATE
+TSharedRef FGitSourceControlProvider::MakeSettingsWidget() const
+{
+ return SNew(SGitSourceControlSettings);
+}
+#endif
+
+ECommandResult::Type FGitSourceControlProvider::ExecuteSynchronousCommand(FGitSourceControlCommand& InCommand, const FText& Task)
+{
+ ECommandResult::Type Result = ECommandResult::Failed;
+
+ // Display the progress dialog if a string was provided
+ {
+ FScopedSourceControlProgress Progress(Task);
+
+ // Issue the command asynchronously...
+ IssueCommand( InCommand );
+
+ // ... then wait for its completion (thus making it synchronous)
+ while(!InCommand.bExecuteProcessed)
+ {
+ // Tick the command queue and update progress.
+ Tick();
+
+ Progress.Tick();
+
+ // Sleep so we don't busy-wait so much.
+ FPlatformProcess::Sleep(0.01f);
+ }
+
+ // always do one more Tick() to make sure the command queue is cleaned up.
+ Tick();
+
+ if(InCommand.bCommandSuccessful)
+ {
+ Result = ECommandResult::Succeeded;
+ }
+ }
+
+ // Delete the command now (asynchronous commands are deleted in the Tick() method)
+ check(!InCommand.bAutoDelete);
+
+ // ensure commands that are not auto deleted do not end up in the command queue
+ if ( CommandQueue.Contains( &InCommand ) )
+ {
+ CommandQueue.Remove( &InCommand );
+ }
+ delete &InCommand;
+
+ return Result;
+}
+
+ECommandResult::Type FGitSourceControlProvider::IssueCommand(FGitSourceControlCommand& InCommand)
+{
+ if(GThreadPool != nullptr)
+ {
+ // Queue this to our worker thread(s) for resolving
+ GThreadPool->AddQueuedWork(&InCommand);
+ CommandQueue.Add(&InCommand);
+ return ECommandResult::Succeeded;
+ }
+ else
+ {
+ FText Message(LOCTEXT("NoSCCThreads", "There are no threads available to process the source control command."));
+
+ FMessageLog("SourceControl").Error(Message);
+ InCommand.bCommandSuccessful = false;
+ InCommand.Operation->AddErrorMessge(Message);
+
+ return InCommand.ReturnResults();
+ }
+}
+
+#undef LOCTEXT_NAMESPACE
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlProvider.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlProvider.h
new file mode 100644
index 0000000..97dce5b
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlProvider.h
@@ -0,0 +1,210 @@
+// 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 "CoreMinimal.h"
+#include "ISourceControlOperation.h"
+#include "ISourceControlState.h"
+#include "ISourceControlProvider.h"
+#include "IGitSourceControlWorker.h"
+#include "GitSourceControlState.h"
+#include "GitSourceControlMenu.h"
+
+class FGitSourceControlCommand;
+
+DECLARE_DELEGATE_RetVal(FGitSourceControlWorkerRef, FGetGitSourceControlWorker)
+
+/// Git version and capabilites extracted from the string "git version 2.11.0.windows.3"
+struct FGitVersion
+{
+ // Git version extracted from the string "git version 2.11.0.windows.3" (Windows) or "git version 2.11.0" (Linux/Mac/Cygwin/WSL)
+ int Major; // 2 Major version number
+ int Minor; // 11 Minor version number
+ int Patch; // 0 Patch/bugfix number
+ int Windows; // 3 Windows specific revision number (under Windows only)
+
+ uint32 bHasCatFileWithFilters : 1;
+ uint32 bHasGitLfs : 1;
+ uint32 bHasGitLfsLocking : 1;
+
+ FGitVersion()
+ : Major(0)
+ , Minor(0)
+ , Patch(0)
+ , Windows(0)
+ , bHasCatFileWithFilters(false)
+ , bHasGitLfs(false)
+ , bHasGitLfsLocking(false)
+ {
+ }
+
+ inline bool IsGreaterOrEqualThan(int InMajor, int InMinor) const
+ {
+ return (Major > InMajor) || (Major == InMajor && (Minor >= InMinor));
+ }
+};
+
+class FGitSourceControlProvider : public ISourceControlProvider
+{
+public:
+ /** Constructor */
+ FGitSourceControlProvider()
+ : bGitAvailable(false)
+ , bGitRepositoryFound(false)
+ {
+ }
+
+ /* ISourceControlProvider implementation */
+ virtual void Init(bool bForceConnection = true) override;
+ virtual void Close() override;
+ virtual FText GetStatusText() const override;
+ virtual bool IsEnabled() const override;
+ virtual bool IsAvailable() const override;
+ virtual const FName& GetName(void) const override;
+ virtual bool QueryStateBranchConfig(const FString& ConfigSrc, const FString& ConfigDest) /* override UE4.20 */ { return false; }
+ virtual void RegisterStateBranches(const TArray& BranchNames, const FString& ContentRoot) /* override UE4.20 */ {}
+ virtual int32 GetStateBranchIndex(const FString& InBranchName) const /* override UE4.20 */ { return INDEX_NONE; }
+ virtual ECommandResult::Type GetState( const TArray& InFiles, TArray< TSharedRef >& OutState, EStateCacheUsage::Type InStateCacheUsage ) override;
+ virtual TArray GetCachedStateByPredicate(TFunctionRef Predicate) const override;
+ virtual FDelegateHandle RegisterSourceControlStateChanged_Handle(const FSourceControlStateChanged::FDelegate& SourceControlStateChanged) override;
+ virtual void UnregisterSourceControlStateChanged_Handle(FDelegateHandle Handle) override;
+ virtual ECommandResult::Type Execute(const TSharedRef& InOperation, const TArray& InFiles, EConcurrency::Type InConcurrency = EConcurrency::Synchronous, const FSourceControlOperationComplete& InOperationCompleteDelegate = FSourceControlOperationComplete()) override;
+ virtual bool CanCancelOperation( const TSharedRef& InOperation ) const override;
+ virtual void CancelOperation( const TSharedRef& InOperation ) override;
+ virtual bool UsesLocalReadOnlyState() const override;
+ virtual bool UsesChangelists() const override;
+ virtual bool UsesCheckout() const override;
+ virtual void Tick() override;
+ virtual TArray< TSharedRef > GetLabels( const FString& InMatchingSpec ) const override;
+#if SOURCE_CONTROL_WITH_SLATE
+ virtual TSharedRef MakeSettingsWidget() const override;
+#endif
+
+ /**
+ * Check configuration, else standard paths, and run a Git "version" command to check the availability of the binary.
+ */
+ void CheckGitAvailability();
+
+ /**
+ * Find the .git/ repository and check it's status.
+ */
+ void CheckRepositoryStatus(const FString& InPathToGitBinary);
+
+ /** Is git binary found and working. */
+ inline bool IsGitAvailable() const
+ {
+ return bGitAvailable;
+ }
+
+ /** Git version for feature checking */
+ inline const FGitVersion& GetGitVersion() const
+ {
+ return GitVersion;
+ }
+
+ /** Get the path to the root of the Git repository: can be the ProjectDir itself, or any parent directory */
+ inline const FString& GetPathToRepositoryRoot() const
+ {
+ return PathToRepositoryRoot;
+ }
+
+ /** Git config user.name */
+ inline const FString& GetUserName() const
+ {
+ return UserName;
+ }
+
+ /** Git config user.email */
+ inline const FString& GetUserEmail() const
+ {
+ return UserEmail;
+ }
+
+ /** Git remote origin url */
+ inline const FString& GetRemoteUrl() const
+ {
+ return RemoteUrl;
+ }
+
+ /** Helper function used to update state cache */
+ TSharedRef GetStateInternal(const FString& Filename);
+
+ /**
+ * Register a worker with the provider.
+ * This is used internally so the provider can maintain a map of all available operations.
+ */
+ void RegisterWorker( const FName& InName, const FGetGitSourceControlWorker& InDelegate );
+
+ /** Remove a named file from the state cache */
+ bool RemoveFileFromCache(const FString& Filename);
+
+ /** Get files in cache */
+ TArray GetFilesInCache();
+
+private:
+
+ /** Is git binary found and working. */
+ bool bGitAvailable;
+
+ /** Is git repository found. */
+ bool bGitRepositoryFound;
+
+ /** Is LFS File Locking enabled? */
+ bool bUsingGitLfsLocking = false;
+
+ /** Helper function for Execute() */
+ TSharedPtr CreateWorker(const FName& InOperationName) const;
+
+ /** Helper function for running command synchronously. */
+ ECommandResult::Type ExecuteSynchronousCommand(class FGitSourceControlCommand& InCommand, const FText& Task);
+ /** Issue a command asynchronously if possible. */
+ ECommandResult::Type IssueCommand(class FGitSourceControlCommand& InCommand);
+
+ /** Output any messages this command holds */
+ void OutputCommandMessages(const class FGitSourceControlCommand& InCommand) const;
+
+ /** Update repository status on Connect and UpdateStatus operations */
+ void UpdateRepositoryStatus(const class FGitSourceControlCommand& InCommand);
+
+ /** Path to the root of the Git repository: can be the ProjectDir itself, or any parent directory (found by the "Connect" operation) */
+ FString PathToRepositoryRoot;
+
+ /** Git config user.name (from local repository, else globally) */
+ FString UserName;
+
+ /** Git config user.email (from local repository, else globally) */
+ FString UserEmail;
+
+ /** Name of the current branch */
+ FString BranchName;
+
+ /** URL of the "origin" defaut remote server */
+ FString RemoteUrl;
+
+ /** Current Commit full SHA1 */
+ FString CommitId;
+
+ /** Current Commit description's Summary */
+ FString CommitSummary;
+
+ /** State cache */
+ TMap > StateCache;
+
+ /** The currently registered source control operations */
+ TMap WorkersMap;
+
+ /** Queue for commands given by the main thread */
+ TArray < FGitSourceControlCommand* > CommandQueue;
+
+ /** For notifying when the source control states in the cache have changed */
+ FSourceControlStateChanged OnSourceControlStateChanged;
+
+ /** Git version for feature checking */
+ FGitVersion GitVersion;
+
+ /** Source Control Menu Extension */
+ FGitSourceControlMenu GitSourceControlMenu;
+};
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlRevision.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlRevision.cpp
new file mode 100644
index 0000000..0858f7f
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlRevision.cpp
@@ -0,0 +1,114 @@
+// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com)
+//
+// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
+// or copy at http://opensource.org/licenses/MIT)
+
+#include "GitSourceControlRevision.h"
+
+#include "HAL/FileManager.h"
+#include "Misc/Paths.h"
+#include "Modules/ModuleManager.h"
+#include "GitSourceControlModule.h"
+#include "GitSourceControlUtils.h"
+
+#define LOCTEXT_NAMESPACE "GitSourceControl"
+
+bool FGitSourceControlRevision::Get( FString& InOutFilename ) const
+{
+ FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl");
+ const FString PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
+ const FString PathToRepositoryRoot = GitSourceControl.GetProvider().GetPathToRepositoryRoot();
+
+ // if a filename for the temp file wasn't supplied generate a unique-ish one
+ if(InOutFilename.Len() == 0)
+ {
+ // create the diff dir if we don't already have it (Git wont)
+ IFileManager::Get().MakeDirectory(*FPaths::DiffDir(), true);
+ // create a unique temp file name based on the unique commit Id
+ const FString TempFileName = FString::Printf(TEXT("%stemp-%s-%s"), *FPaths::DiffDir(), *CommitId, *FPaths::GetCleanFilename(Filename));
+ InOutFilename = FPaths::ConvertRelativePathToFull(TempFileName);
+ }
+
+ // Diff against the revision
+ const FString Parameter = FString::Printf(TEXT("%s:%s"), *CommitId, *Filename);
+
+ bool bCommandSuccessful;
+ if(FPaths::FileExists(InOutFilename))
+ {
+ bCommandSuccessful = true; // if the temp file already exists, reuse it directly
+ }
+ else
+ {
+ bCommandSuccessful = GitSourceControlUtils::RunDumpToFile(PathToGitBinary, PathToRepositoryRoot, Parameter, InOutFilename);
+ }
+ return bCommandSuccessful;
+}
+
+bool FGitSourceControlRevision::GetAnnotated( TArray& OutLines ) const
+{
+ return false;
+}
+
+bool FGitSourceControlRevision::GetAnnotated( FString& InOutFilename ) const
+{
+ return false;
+}
+
+const FString& FGitSourceControlRevision::GetFilename() const
+{
+ return Filename;
+}
+
+int32 FGitSourceControlRevision::GetRevisionNumber() const
+{
+ return RevisionNumber;
+}
+
+const FString& FGitSourceControlRevision::GetRevision() const
+{
+ return ShortCommitId;
+}
+
+const FString& FGitSourceControlRevision::GetDescription() const
+{
+ return Description;
+}
+
+const FString& FGitSourceControlRevision::GetUserName() const
+{
+ return UserName;
+}
+
+const FString& FGitSourceControlRevision::GetClientSpec() const
+{
+ static FString EmptyString(TEXT(""));
+ return EmptyString;
+}
+
+const FString& FGitSourceControlRevision::GetAction() const
+{
+ return Action;
+}
+
+TSharedPtr FGitSourceControlRevision::GetBranchSource() const
+{
+ // if this revision was copied/moved from some other revision
+ return BranchSource;
+}
+
+const FDateTime& FGitSourceControlRevision::GetDate() const
+{
+ return Date;
+}
+
+int32 FGitSourceControlRevision::GetCheckInIdentifier() const
+{
+ return CommitIdNumber;
+}
+
+int32 FGitSourceControlRevision::GetFileSize() const
+{
+ return FileSize;
+}
+
+#undef LOCTEXT_NAMESPACE
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlRevision.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlRevision.h
new file mode 100644
index 0000000..d2cf7ab
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlRevision.h
@@ -0,0 +1,76 @@
+// 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 "CoreMinimal.h"
+#include "ISourceControlRevision.h"
+
+/** Revision of a file, linked to a specific commit */
+class FGitSourceControlRevision : public ISourceControlRevision, public TSharedFromThis
+{
+public:
+ FGitSourceControlRevision()
+ : RevisionNumber(0)
+ {
+ }
+
+ /** ISourceControlRevision interface */
+ virtual bool Get( FString& InOutFilename ) const override;
+ virtual bool GetAnnotated( TArray& OutLines ) const override;
+ virtual bool GetAnnotated( FString& InOutFilename ) const override;
+ virtual const FString& GetFilename() const override;
+ virtual int32 GetRevisionNumber() const override;
+ virtual const FString& GetRevision() const override;
+ virtual const FString& GetDescription() const override;
+ virtual const FString& GetUserName() const override;
+ virtual const FString& GetClientSpec() const override;
+ virtual const FString& GetAction() const override;
+ virtual TSharedPtr GetBranchSource() const override;
+ virtual const FDateTime& GetDate() const override;
+ virtual int32 GetCheckInIdentifier() const override;
+ virtual int32 GetFileSize() const override;
+
+public:
+
+ /** The filename this revision refers to */
+ FString Filename;
+
+ /** The full hexadecimal SHA1 id of the commit this revision refers to */
+ FString CommitId;
+
+ /** The short hexadecimal SHA1 id (8 first hex char out of 40) of the commit: the string to display */
+ FString ShortCommitId;
+
+ /** The numeric value of the short SHA1 (8 first hex char out of 40) */
+ int32 CommitIdNumber;
+
+ /** The index of the revision in the history (SBlueprintRevisionMenu assumes order for the "Depot" label) */
+ int32 RevisionNumber;
+
+ /** The SHA1 identifier of the file at this revision */
+ FString FileHash;
+
+ /** The description of this revision */
+ FString Description;
+
+ /** The user that made the change */
+ FString UserName;
+
+ /** The action (add, edit, branch etc.) performed at this revision */
+ FString Action;
+
+ /** Source of move ("branch" in Perforce term) if any */
+ TSharedPtr BranchSource;
+
+ /** The date this revision was made */
+ FDateTime Date;
+
+ /** The size of the file at this revision */
+ int32 FileSize;
+};
+
+/** History composed of the last 100 revisions of the file */
+typedef TArray< TSharedRef > TGitSourceControlHistory;
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlSettings.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlSettings.cpp
new file mode 100644
index 0000000..b03c6e5
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlSettings.cpp
@@ -0,0 +1,90 @@
+// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com)
+//
+// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
+// or copy at http://opensource.org/licenses/MIT)
+
+#include "GitSourceControlSettings.h"
+
+#include "Misc/ScopeLock.h"
+#include "Misc/ConfigCacheIni.h"
+#include "Modules/ModuleManager.h"
+#include "SourceControlHelpers.h"
+#include "GitSourceControlModule.h"
+#include "GitSourceControlUtils.h"
+
+namespace GitSettingsConstants
+{
+
+/** The section of the ini file we load our settings from */
+static const FString SettingsSection = TEXT("GitSourceControl.GitSourceControlSettings");
+
+}
+
+const FString FGitSourceControlSettings::GetBinaryPath() const
+{
+ FScopeLock ScopeLock(&CriticalSection);
+ return BinaryPath; // Return a copy to be thread-safe
+}
+
+bool FGitSourceControlSettings::SetBinaryPath(const FString& InString)
+{
+ FScopeLock ScopeLock(&CriticalSection);
+ const bool bChanged = (BinaryPath != InString);
+ if(bChanged)
+ {
+ BinaryPath = InString;
+ }
+ return bChanged;
+}
+
+/** Tell if using the Git LFS file Locking workflow */
+bool FGitSourceControlSettings::IsUsingGitLfsLocking() const
+{
+ FScopeLock ScopeLock(&CriticalSection);
+ return bUsingGitLfsLocking;
+}
+
+/** Configure the usage of Git LFS file Locking workflow */
+bool FGitSourceControlSettings::SetUsingGitLfsLocking(const bool InUsingGitLfsLocking)
+{
+ FScopeLock ScopeLock(&CriticalSection);
+ const bool bChanged = (bUsingGitLfsLocking != InUsingGitLfsLocking);
+ bUsingGitLfsLocking = InUsingGitLfsLocking;
+ return bChanged;
+}
+
+const FString FGitSourceControlSettings::GetLfsUserName() const
+{
+ FScopeLock ScopeLock(&CriticalSection);
+ return LfsUserName; // Return a copy to be thread-safe
+}
+
+bool FGitSourceControlSettings::SetLfsUserName(const FString& InString)
+{
+ FScopeLock ScopeLock(&CriticalSection);
+ const bool bChanged = (LfsUserName != InString);
+ if (bChanged)
+ {
+ LfsUserName = InString;
+ }
+ return bChanged;
+}
+
+// This is called at startup nearly before anything else in our module: BinaryPath will then be used by the provider
+void FGitSourceControlSettings::LoadSettings()
+{
+ FScopeLock ScopeLock(&CriticalSection);
+ const FString& IniFile = SourceControlHelpers::GetSettingsIni();
+ GConfig->GetString(*GitSettingsConstants::SettingsSection, TEXT("BinaryPath"), BinaryPath, IniFile);
+ GConfig->GetBool(*GitSettingsConstants::SettingsSection, TEXT("UsingGitLfsLocking"), bUsingGitLfsLocking, IniFile);
+ GConfig->GetString(*GitSettingsConstants::SettingsSection, TEXT("LfsUserName"), LfsUserName, IniFile);
+}
+
+void FGitSourceControlSettings::SaveSettings() const
+{
+ FScopeLock ScopeLock(&CriticalSection);
+ const FString& IniFile = SourceControlHelpers::GetSettingsIni();
+ GConfig->SetString(*GitSettingsConstants::SettingsSection, TEXT("BinaryPath"), *BinaryPath, IniFile);
+ GConfig->SetBool(*GitSettingsConstants::SettingsSection, TEXT("UsingGitLfsLocking"), bUsingGitLfsLocking, IniFile);
+ GConfig->SetString(*GitSettingsConstants::SettingsSection, TEXT("LfsUserName"), *LfsUserName, IniFile);
+}
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlSettings.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlSettings.h
new file mode 100644
index 0000000..3a9549f
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlSettings.h
@@ -0,0 +1,49 @@
+// 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 "CoreMinimal.h"
+
+class FGitSourceControlSettings
+{
+public:
+ /** Get the Git Binary Path */
+ const FString GetBinaryPath() const;
+
+ /** Set the Git Binary Path */
+ bool SetBinaryPath(const FString& InString);
+
+ /** Tell if using the Git LFS file Locking workflow */
+ bool IsUsingGitLfsLocking() const;
+
+ /** Configure the usage of Git LFS file Locking workflow */
+ bool SetUsingGitLfsLocking(const bool InUsingGitLfsLocking);
+
+ /** Get the username used by the Git LFS 2 File Locks server */
+ const FString GetLfsUserName() const;
+
+ /** Set the username used by the Git LFS 2 File Locks server */
+ bool SetLfsUserName(const FString& InString);
+
+ /** Load settings from ini file */
+ void LoadSettings();
+
+ /** Save settings to ini file */
+ void SaveSettings() const;
+
+private:
+ /** A critical section for settings access */
+ mutable FCriticalSection CriticalSection;
+
+ /** Git binary path */
+ FString BinaryPath;
+
+ /** Tells if using the Git LFS file Locking workflow */
+ bool bUsingGitLfsLocking;
+
+ /** Username used by the Git LFS 2 File Locks server */
+ FString LfsUserName;
+};
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlState.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlState.cpp
new file mode 100644
index 0000000..8801ffd
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlState.cpp
@@ -0,0 +1,386 @@
+// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com)
+//
+// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
+// or copy at http://opensource.org/licenses/MIT)
+
+#include "GitSourceControlState.h"
+
+#define LOCTEXT_NAMESPACE "GitSourceControl.State"
+
+int32 FGitSourceControlState::GetHistorySize() const
+{
+ return History.Num();
+}
+
+TSharedPtr FGitSourceControlState::GetHistoryItem( int32 HistoryIndex ) const
+{
+ check(History.IsValidIndex(HistoryIndex));
+ return History[HistoryIndex];
+}
+
+TSharedPtr FGitSourceControlState::FindHistoryRevision( int32 RevisionNumber ) const
+{
+ for(const auto& Revision : History)
+ {
+ if(Revision->GetRevisionNumber() == RevisionNumber)
+ {
+ return Revision;
+ }
+ }
+
+ return nullptr;
+}
+
+TSharedPtr FGitSourceControlState::FindHistoryRevision(const FString& InRevision) const
+{
+ for(const auto& Revision : History)
+ {
+ if(Revision->GetRevision() == InRevision)
+ {
+ return Revision;
+ }
+ }
+
+ return nullptr;
+}
+
+TSharedPtr FGitSourceControlState::GetBaseRevForMerge() const
+{
+ for(const auto& Revision : History)
+ {
+ // look for the the SHA1 id of the file, not the commit id (revision)
+ if(Revision->FileHash == PendingMergeBaseFileHash)
+ {
+ return Revision;
+ }
+ }
+
+ return nullptr;
+}
+
+// @todo add Slate icons for git specific states (NotAtHead vs Conflicted...)
+FName FGitSourceControlState::GetIconName() const
+{
+ if(LockState == ELockState::Locked)
+ {
+ return FName("Subversion.CheckedOut");
+ }
+ else if(LockState == ELockState::LockedOther)
+ {
+ return FName("Subversion.CheckedOutByOtherUser");
+ }
+ else if (!IsCurrent())
+ {
+ return FName("Subversion.NotAtHeadRevision");
+ }
+
+ switch(WorkingCopyState)
+ {
+ case EWorkingCopyState::Modified:
+ if(bUsingGitLfsLocking)
+ {
+ return FName("Subversion.NotInDepot");
+ }
+ else
+ {
+ return FName("Subversion.CheckedOut");
+ }
+ case EWorkingCopyState::Added:
+ return FName("Subversion.OpenForAdd");
+ case EWorkingCopyState::Renamed:
+ case EWorkingCopyState::Copied:
+ return FName("Subversion.Branched");
+ case EWorkingCopyState::Deleted: // Deleted & Missing files does not show in Content Browser
+ case EWorkingCopyState::Missing:
+ return FName("Subversion.MarkedForDelete");
+ case EWorkingCopyState::Conflicted:
+ return FName("Subversion.ModifiedOtherBranch");
+ case EWorkingCopyState::NotControlled:
+ return FName("Subversion.NotInDepot");
+ case EWorkingCopyState::Unknown:
+ case EWorkingCopyState::Unchanged: // Unchanged is the same as "Pristine" (not checked out) for Perforce, ie no icon
+ case EWorkingCopyState::Ignored:
+ default:
+ return NAME_None;
+ }
+
+ return NAME_None;
+}
+
+FName FGitSourceControlState::GetSmallIconName() const
+{
+ if(LockState == ELockState::Locked)
+ {
+ return FName("Subversion.CheckedOut_Small");
+ }
+ else if(LockState == ELockState::LockedOther)
+ {
+ return FName("Subversion.CheckedOutByOtherUser_Small");
+ }
+ else if (!IsCurrent())
+ {
+ return FName("Subversion.NotAtHeadRevision_Small");
+ }
+
+ switch(WorkingCopyState)
+ {
+ case EWorkingCopyState::Modified:
+ if(bUsingGitLfsLocking)
+ {
+ return FName("Subversion.NotInDepot_Small");
+ }
+ else
+ {
+ return FName("Subversion.CheckedOut_Small");
+ }
+ case EWorkingCopyState::Added:
+ return FName("Subversion.OpenForAdd_Small");
+ case EWorkingCopyState::Renamed:
+ case EWorkingCopyState::Copied:
+ return FName("Subversion.Branched_Small");
+ case EWorkingCopyState::Deleted: // Deleted & Missing files can appear in the Submit to Source Control window
+ case EWorkingCopyState::Missing:
+ return FName("Subversion.MarkedForDelete_Small");
+ case EWorkingCopyState::Conflicted:
+ return FName("Subversion.ModifiedOtherBranch_Small");
+ case EWorkingCopyState::NotControlled:
+ return FName("Subversion.NotInDepot_Small");
+ case EWorkingCopyState::Unknown:
+ case EWorkingCopyState::Unchanged: // Unchanged is the same as "Pristine" (not checked out) for Perforce, ie no icon
+ case EWorkingCopyState::Ignored:
+ default:
+ return NAME_None;
+ }
+
+ return NAME_None;
+}
+
+FText FGitSourceControlState::GetDisplayName() const
+{
+ if(LockState == ELockState::Locked)
+ {
+ return LOCTEXT("Locked", "Locked For Editing");
+ }
+ else if(LockState == ELockState::LockedOther)
+ {
+ return FText::Format( LOCTEXT("LockedOther", "Locked by "), FText::FromString(LockUser) );
+ }
+ else if (!IsCurrent())
+ {
+ return LOCTEXT("NotCurrent", "Not current");
+ }
+
+ switch(WorkingCopyState)
+ {
+ case EWorkingCopyState::Unknown:
+ return LOCTEXT("Unknown", "Unknown");
+ case EWorkingCopyState::Unchanged:
+ return LOCTEXT("Unchanged", "Unchanged");
+ case EWorkingCopyState::Added:
+ return LOCTEXT("Added", "Added");
+ case EWorkingCopyState::Deleted:
+ return LOCTEXT("Deleted", "Deleted");
+ case EWorkingCopyState::Modified:
+ return LOCTEXT("Modified", "Modified");
+ case EWorkingCopyState::Renamed:
+ return LOCTEXT("Renamed", "Renamed");
+ case EWorkingCopyState::Copied:
+ return LOCTEXT("Copied", "Copied");
+ case EWorkingCopyState::Conflicted:
+ return LOCTEXT("ContentsConflict", "Contents Conflict");
+ case EWorkingCopyState::Ignored:
+ return LOCTEXT("Ignored", "Ignored");
+ case EWorkingCopyState::NotControlled:
+ return LOCTEXT("NotControlled", "Not Under Source Control");
+ case EWorkingCopyState::Missing:
+ return LOCTEXT("Missing", "Missing");
+ }
+
+ return FText();
+}
+
+FText FGitSourceControlState::GetDisplayTooltip() const
+{
+ if(LockState == ELockState::Locked)
+ {
+ return LOCTEXT("Locked_Tooltip", "Locked for editing by current user");
+ }
+ else if(LockState == ELockState::LockedOther)
+ {
+ return FText::Format( LOCTEXT("LockedOther_Tooltip", "Locked for editing by: {0}"), FText::FromString(LockUser) );
+ }
+ else if (!IsCurrent())
+ {
+ return LOCTEXT("NotCurrent_Tooltip", "The file(s) are not at the head revision");
+ }
+
+ switch(WorkingCopyState)
+ {
+ case EWorkingCopyState::Unknown:
+ return LOCTEXT("Unknown_Tooltip", "Unknown source control state");
+ case EWorkingCopyState::Unchanged:
+ return LOCTEXT("Pristine_Tooltip", "There are no modifications");
+ case EWorkingCopyState::Added:
+ return LOCTEXT("Added_Tooltip", "Item is scheduled for addition");
+ case EWorkingCopyState::Deleted:
+ return LOCTEXT("Deleted_Tooltip", "Item is scheduled for deletion");
+ case EWorkingCopyState::Modified:
+ return LOCTEXT("Modified_Tooltip", "Item has been modified");
+ case EWorkingCopyState::Renamed:
+ return LOCTEXT("Renamed_Tooltip", "Item has been renamed");
+ case EWorkingCopyState::Copied:
+ return LOCTEXT("Copied_Tooltip", "Item has been copied");
+ case EWorkingCopyState::Conflicted:
+ return LOCTEXT("ContentsConflict_Tooltip", "The contents of the item conflict with updates received from the repository.");
+ case EWorkingCopyState::Ignored:
+ return LOCTEXT("Ignored_Tooltip", "Item is being ignored.");
+ case EWorkingCopyState::NotControlled:
+ return LOCTEXT("NotControlled_Tooltip", "Item is not under version control.");
+ case EWorkingCopyState::Missing:
+ return LOCTEXT("Missing_Tooltip", "Item is missing (e.g., you moved or deleted it without using Git). This also indicates that a directory is incomplete (a checkout or update was interrupted).");
+ }
+
+ return FText();
+}
+
+const FString& FGitSourceControlState::GetFilename() const
+{
+ return LocalFilename;
+}
+
+const FDateTime& FGitSourceControlState::GetTimeStamp() const
+{
+ return TimeStamp;
+}
+
+// Deleted and Missing assets cannot appear in the Content Browser, but the do in the Submit files to Source Control window!
+bool FGitSourceControlState::CanCheckIn() const
+{
+ if(bUsingGitLfsLocking)
+ {
+ return ( ( (LockState == ELockState::Locked) && !IsConflicted() ) || (WorkingCopyState == EWorkingCopyState::Added) ) && IsCurrent();
+ }
+ else
+ {
+ return (WorkingCopyState == EWorkingCopyState::Added
+ || WorkingCopyState == EWorkingCopyState::Deleted
+ || WorkingCopyState == EWorkingCopyState::Missing
+ || WorkingCopyState == EWorkingCopyState::Modified
+ || WorkingCopyState == EWorkingCopyState::Renamed) && IsCurrent();
+ }
+}
+
+bool FGitSourceControlState::CanCheckout() const
+{
+ if(bUsingGitLfsLocking)
+ {
+ // We don't want to allow checkout if the file is out-of-date, as modifying an out-of-date binary file will most likely result in a merge conflict
+ return (WorkingCopyState == EWorkingCopyState::Unchanged || WorkingCopyState == EWorkingCopyState::Modified) && LockState == ELockState::NotLocked && IsCurrent();
+ }
+ else
+ {
+ return false; // With Git all tracked files in the working copy are always already checked-out (as opposed to Perforce)
+ }
+}
+
+bool FGitSourceControlState::IsCheckedOut() const
+{
+ if (bUsingGitLfsLocking)
+ {
+ return LockState == ELockState::Locked;
+ }
+ else
+ {
+ return IsSourceControlled(); // With Git all tracked files in the working copy are always checked-out (as opposed to Perforce)
+ }
+}
+
+bool FGitSourceControlState::IsCheckedOutOther(FString* Who) const
+{
+ if (Who != NULL)
+ {
+ *Who = LockUser;
+ }
+ return LockState == ELockState::LockedOther;
+}
+
+bool FGitSourceControlState::IsCurrent() const
+{
+ return !bNewerVersionOnServer;
+}
+
+bool FGitSourceControlState::IsSourceControlled() const
+{
+ return WorkingCopyState != EWorkingCopyState::NotControlled && WorkingCopyState != EWorkingCopyState::Ignored && WorkingCopyState != EWorkingCopyState::Unknown;
+}
+
+bool FGitSourceControlState::IsAdded() const
+{
+ return WorkingCopyState == EWorkingCopyState::Added;
+}
+
+bool FGitSourceControlState::IsDeleted() const
+{
+ return WorkingCopyState == EWorkingCopyState::Deleted || WorkingCopyState == EWorkingCopyState::Missing;
+}
+
+bool FGitSourceControlState::IsIgnored() const
+{
+ return WorkingCopyState == EWorkingCopyState::Ignored;
+}
+
+bool FGitSourceControlState::CanEdit() const
+{
+ return IsCurrent(); // With Git all files in the working copy are always editable (as opposed to Perforce)
+}
+
+bool FGitSourceControlState::CanDelete() const
+{
+ return !IsCheckedOutOther() && IsSourceControlled() && IsCurrent();
+}
+
+bool FGitSourceControlState::IsUnknown() const
+{
+ return WorkingCopyState == EWorkingCopyState::Unknown;
+}
+
+bool FGitSourceControlState::IsModified() const
+{
+ // Warning: for Perforce, a checked-out file is locked for modification (whereas with Git all tracked files are checked-out),
+ // so for a clean "check-in" (commit) checked-out files unmodified should be removed from the changeset (the index)
+ // http://stackoverflow.com/questions/12357971/what-does-revert-unchanged-files-mean-in-perforce
+ //
+ // Thus, before check-in UE4 Editor call RevertUnchangedFiles() in PromptForCheckin() and CheckinFiles().
+ //
+ // So here we must take care to enumerate all states that need to be commited,
+ // all other will be discarded :
+ // - Unknown
+ // - Unchanged
+ // - NotControlled
+ // - Ignored
+ return WorkingCopyState == EWorkingCopyState::Added
+ || WorkingCopyState == EWorkingCopyState::Deleted
+ || WorkingCopyState == EWorkingCopyState::Modified
+ || WorkingCopyState == EWorkingCopyState::Renamed
+ || WorkingCopyState == EWorkingCopyState::Copied
+ || WorkingCopyState == EWorkingCopyState::Missing
+ || WorkingCopyState == EWorkingCopyState::Conflicted;
+}
+
+
+bool FGitSourceControlState::CanAdd() const
+{
+ return WorkingCopyState == EWorkingCopyState::NotControlled;
+}
+
+bool FGitSourceControlState::IsConflicted() const
+{
+ return WorkingCopyState == EWorkingCopyState::Conflicted;
+}
+
+bool FGitSourceControlState::CanRevert() const
+{
+ return CanCheckIn();
+}
+
+#undef LOCTEXT_NAMESPACE
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlState.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlState.h
new file mode 100644
index 0000000..d1ea0fa
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlState.h
@@ -0,0 +1,117 @@
+// 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 "CoreMinimal.h"
+#include "ISourceControlState.h"
+#include "ISourceControlRevision.h"
+#include "GitSourceControlRevision.h"
+
+namespace EWorkingCopyState
+{
+ enum Type
+ {
+ Unknown,
+ Unchanged, // called "clean" in SVN, "Pristine" in Perforce
+ Added,
+ Deleted,
+ Modified,
+ Renamed,
+ Copied,
+ Missing,
+ Conflicted,
+ NotControlled,
+ Ignored,
+ };
+}
+
+namespace ELockState
+{
+ enum Type
+ {
+ Unknown,
+ NotLocked,
+ Locked,
+ LockedOther,
+ };
+}
+
+class FGitSourceControlState : public ISourceControlState, public TSharedFromThis
+{
+public:
+ FGitSourceControlState( const FString& InLocalFilename, const bool InUsingLfsLocking)
+ : LocalFilename(InLocalFilename)
+ , WorkingCopyState(EWorkingCopyState::Unknown)
+ , LockState(ELockState::Unknown)
+ , bUsingGitLfsLocking(InUsingLfsLocking)
+ , bNewerVersionOnServer(false)
+ , TimeStamp(0)
+ {
+ }
+
+ /** ISourceControlState interface */
+ virtual int32 GetHistorySize() const override;
+ virtual TSharedPtr GetHistoryItem(int32 HistoryIndex) const override;
+ virtual TSharedPtr FindHistoryRevision(int32 RevisionNumber) const override;
+ virtual TSharedPtr FindHistoryRevision(const FString& InRevision) const override;
+ virtual TSharedPtr GetBaseRevForMerge() const override;
+ virtual FName GetIconName() const override;
+ virtual FName GetSmallIconName() const override;
+ virtual FText GetDisplayName() const override;
+ virtual FText GetDisplayTooltip() const override;
+ virtual const FString& GetFilename() const override;
+ virtual const FDateTime& GetTimeStamp() const override;
+ virtual bool CanCheckIn() const override;
+ virtual bool CanCheckout() const override;
+ virtual bool IsCheckedOut() const override;
+ virtual bool IsCheckedOutOther(FString* Who = nullptr) const override;
+ virtual bool IsCheckedOutInOtherBranch(const FString& CurrentBranch = FString()) const /* UE4.20 override */ { return false; }
+ virtual bool IsModifiedInOtherBranch(const FString& CurrentBranch = FString()) const /* UE4.20 override */ { return false; }
+ virtual bool IsCheckedOutOrModifiedInOtherBranch(const FString& CurrentBranch = FString()) const /* UE4.20 override */ { return IsCheckedOutInOtherBranch(CurrentBranch) || IsModifiedInOtherBranch(CurrentBranch); }
+ virtual TArray GetCheckedOutBranches() const /* UE4.20 override */ { return TArray(); }
+ virtual FString GetOtherUserBranchCheckedOuts() const /* UE4.20 override */ { return FString(); }
+ virtual bool GetOtherBranchHeadModification(FString& HeadBranchOut, FString& ActionOut, int32& HeadChangeListOut) const /* UE4.20 override */ { return false; }
+ virtual bool IsCurrent() const override;
+ virtual bool IsSourceControlled() const override;
+ virtual bool IsAdded() const override;
+ virtual bool IsDeleted() const override;
+ virtual bool IsIgnored() const override;
+ virtual bool CanEdit() const override;
+ virtual bool CanDelete() const override;
+ virtual bool IsUnknown() const override;
+ virtual bool IsModified() const override;
+ virtual bool CanAdd() const override;
+ virtual bool IsConflicted() const override;
+ virtual bool CanRevert() const override;
+
+public:
+ /** History of the item, if any */
+ TGitSourceControlHistory History;
+
+ /** Filename on disk */
+ FString LocalFilename;
+
+ /** File Id with which our local revision diverged from the remote revision */
+ FString PendingMergeBaseFileHash;
+
+ /** State of the working copy */
+ EWorkingCopyState::Type WorkingCopyState;
+
+ /** Lock state */
+ ELockState::Type LockState;
+
+ /** Name of user who has locked the file */
+ FString LockUser;
+
+ /** Tells if using the Git LFS file Locking workflow */
+ bool bUsingGitLfsLocking;
+
+ /** Whether a newer version exists on the server */
+ bool bNewerVersionOnServer;
+
+ /** The timestamp of the last update */
+ FDateTime TimeStamp;
+};
diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlUtils.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlUtils.cpp
new file mode 100644
index 0000000..4201458
--- /dev/null
+++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlUtils.cpp
@@ -0,0 +1,1556 @@
+// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com)
+//
+// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
+// or copy at http://opensource.org/licenses/MIT)
+
+#include "GitSourceControlUtils.h"
+
+#include "GitSourceControlCommand.h"
+#include "HAL/PlatformProcess.h"
+#include "HAL/PlatformFilemanager.h"
+#include "HAL/FileManager.h"
+#include "Misc/FileHelper.h"
+#include "Misc/Paths.h"
+#include "Modules/ModuleManager.h"
+#include "ISourceControlModule.h"
+#include "GitSourceControlModule.h"
+#include "GitSourceControlProvider.h"
+
+#if PLATFORM_LINUX
+#include
+#endif
+
+
+namespace GitSourceControlConstants
+{
+ /** The maximum number of files we submit in a single Git command */
+ const int32 MaxFilesPerBatch = 50;
+}
+
+FGitScopedTempFile::FGitScopedTempFile(const FText& InText)
+{
+ Filename = FPaths::CreateTempFilename(*FPaths::ProjectLogDir(), TEXT("Git-Temp"), TEXT(".txt"));
+ if(!FFileHelper::SaveStringToFile(InText.ToString(), *Filename, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
+ {
+ UE_LOG(LogSourceControl, Error, TEXT("Failed to write to temp file: %s"), *Filename);
+ }
+}
+
+FGitScopedTempFile::~FGitScopedTempFile()
+{
+ if(FPaths::FileExists(Filename))
+ {
+ if(!FPlatformFileManager::Get().GetPlatformFile().DeleteFile(*Filename))
+ {
+ UE_LOG(LogSourceControl, Error, TEXT("Failed to delete temp file: %s"), *Filename);
+ }
+ }
+}
+
+const FString& FGitScopedTempFile::GetFilename() const
+{
+ return Filename;
+}
+
+
+namespace GitSourceControlUtils
+{
+
+// Launch the Git command line process and extract its results & errors
+static 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())
+ {
+ 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) )
+ {
+ // 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;
+
+ // 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;
+
+ UE_LOG(LogSourceControl, Log, TEXT("RunCommand: 'git %s'"), *LogableCommand);
+
+ 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
+ FPlatformProcess::ExecProcess(*PathToGitOrEnvBinary, *FullCommand, &ReturnCode, &OutResults, &OutErrors);
+
+ // TODO: add a setting to easily enable Verbose logging
+ UE_LOG(LogSourceControl, Verbose, TEXT("RunCommand(%s):\n%s"), *InCommand, *OutResults);
+ if(ReturnCode != ExpectedReturnCode || OutErrors.Len() > 0)
+ {
+ UE_LOG(LogSourceControl, Warning, TEXT("RunCommand(%s) ReturnCode=%d:\n%s"), *InCommand, ReturnCode, *OutErrors);
+ }
+
+ // 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;
+}
+
+// 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;
+
+ bResult = RunCommandInternalRaw(InCommand, InPathToGitBinary, InRepositoryRoot, InParameters, InFiles, Results, Errors);
+ Results.ParseIntoArray(OutResults, TEXT("\n"), true);
+ Errors.ParseIntoArray(OutErrorMessages, TEXT("\n"), true);
+
+ return bResult;
+}
+
+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);
+ }
+
+ // 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 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);
+ }
+ }
+
+ // 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
+ 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);
+ }
+
+#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
+ 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);
+ }
+ }
+
+ // 6) Else, look for the version of local_git provided by SourceTree
+ if (!bFound)
+ {
+ NSURL* AppURL = [SharedWorkspace URLForApplicationWithBundleIdentifier:@"com.torusknot.SourceTreeNotMAS"];
+ if (AppURL != nullptr)
+ {
+ NSBundle* Bundle = [NSBundle bundleWithURL:AppURL];
+ GitBinaryPath = FString::Printf(TEXT("%s/git_local/bin/git"), *FString([Bundle resourcePath]));
+ bFound = CheckGitAvailability(GitBinaryPath);
+ }
+ }
+
+ // 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)
+ {
+ NSBundle* Bundle = [NSBundle bundleWithURL:AppURL];
+ GitBinaryPath = FString::Printf(TEXT("%s/git/bin/git"), *FString([Bundle resourcePath]));
+ 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();
+ }
+
+ return GitBinaryPath;
+}
+
+bool CheckGitAvailability(const FString& InPathToGitBinary, FGitVersion *OutVersion)
+{
+ FString InfoMessages;
+ FString ErrorMessages;
+ bool bGitAvailable = RunCommandInternalRaw(TEXT("version"), InPathToGitBinary, FString(), TArray(), TArray(), InfoMessages, ErrorMessages);
+ if(bGitAvailable)
+ {
+ if(!InfoMessages.Contains("git"))
+ {
+ bGitAvailable = false;
+ }
+ else if(OutVersion)
+ {
+ ParseGitVersion(InfoMessages, OutVersion);
+ FindGitCapabilities(InPathToGitBinary, OutVersion);
+ FindGitLfsCapabilities(InPathToGitBinary, OutVersion);
+ }
+ }
+
+ return bGitAvailable;
+}
+
+void ParseGitVersion(const FString& InVersionString, FGitVersion *OutVersion)
+{
+ // Parse "git version 2.11.0.windows.3" into the string tokens "git", "version", "2.11.0.windows.3"
+ TArray TokenizedString;
+ InVersionString.ParseIntoArrayWS(TokenizedString);
+
+ // Select the string token containing the version "2.11.0.windows.3"
+ const FString* TokenVersionStringPtr = TokenizedString.FindByPredicate([](FString& s) { return TChar::IsDigit(s[0]); });
+ if(TokenVersionStringPtr)
+ {
+ // Parse the version into its numerical components
+ TArray ParsedVersionString;
+ TokenVersionStringPtr->ParseIntoArray(ParsedVersionString, TEXT("."));
+ if(ParsedVersionString.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(ParsedVersionString.Num() >= 5)
+ {
+ if((ParsedVersionString[3] == TEXT("windows")) && ParsedVersionString[4].IsNumeric())
+ {
+ OutVersion->Windows = FCString::Atoi(*ParsedVersionString[4]);
+ }
+ }
+ UE_LOG(LogSourceControl, Log, TEXT("Git version %d.%d.%d(%d)"), OutVersion->Major, OutVersion->Minor, OutVersion->Patch, OutVersion->Windows);
+ }
+ }
+ }
+}
+
+void FindGitCapabilities(const FString& InPathToGitBinary, FGitVersion *OutVersion)
+{
+ FString InfoMessages;
+ FString ErrorMessages;
+ RunCommandInternalRaw(TEXT("cat-file -h"), InPathToGitBinary, FString(), TArray(), TArray(), InfoMessages, ErrorMessages, 129);
+ if (InfoMessages.Contains("--filters"))
+ {
+ OutVersion->bHasCatFileWithFilters = true;
+ }
+}
+
+void FindGitLfsCapabilities(const FString& InPathToGitBinary, FGitVersion *OutVersion)
+{
+ FString InfoMessages;
+ FString ErrorMessages;
+ bool bGitLfsAvailable = RunCommandInternalRaw(TEXT("lfs version"), InPathToGitBinary, FString(), TArray(), TArray(), InfoMessages, ErrorMessages);
+ if(bGitLfsAvailable)
+ {
+ OutVersion->bHasGitLfs = true;
+
+ if(InfoMessages.Compare(TEXT("git-lfs/2.0.0")) >= 0)
+ {
+ OutVersion->bHasGitLfsLocking = true; // Git LFS File Locking workflow introduced in "git-lfs/2.0.0"
+ }
+ UE_LOG(LogSourceControl, Log, TEXT("%s"), *InfoMessages);
+ }
+}
+
+// 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)
+{
+ bool bFound = false;
+ FString PathToGitSubdirectory;
+ 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, '/');
+
+ 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;
+}
+
+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, TArray(), InfoMessages, ErrorMessages);
+ if(bResults && InfoMessages.Num() > 0)
+ {
+ OutUserName = InfoMessages[0];
+ }
+
+ Parameters.Reset();
+ Parameters.Add(TEXT("user.email"));
+ InfoMessages.Reset();
+ bResults &= RunCommandInternal(TEXT("config"), InPathToGitBinary, InRepositoryRoot, Parameters, TArray(), InfoMessages, ErrorMessages);
+ if(bResults && InfoMessages.Num() > 0)
+ {
+ OutUserEmail = InfoMessages[0];
+ }
+}
+
+bool GetBranchName(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutBranchName)
+{
+ 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 = RunCommandInternal(TEXT("symbolic-ref"), InPathToGitBinary, InRepositoryRoot, Parameters, TArray(), InfoMessages, ErrorMessages);
+ if(bResults && InfoMessages.Num() > 0)
+ {
+ OutBranchName = InfoMessages[0];
+ }
+ else
+ {
+ Parameters.Reset();
+ Parameters.Add(TEXT("-1"));
+ Parameters.Add(TEXT("--format=\"%h\"")); // no error message while in detached HEAD
+ bResults = RunCommandInternal(TEXT("log"), InPathToGitBinary, InRepositoryRoot, Parameters, TArray(), InfoMessages, ErrorMessages);
+ if(bResults && InfoMessages.Num() > 0)
+ {
+ OutBranchName = "HEAD detached at ";
+ OutBranchName += InfoMessages[0];
+ }
+ else
+ {
+ bResults = false;
+ }
+ }
+
+ return bResults;
+}
+
+bool GetCommitInfo(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutCommitId, FString& OutCommitSummary)
+{
+ bool bResults;
+ TArray InfoMessages;
+ TArray