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 ErrorMessages; + TArray Parameters; + Parameters.Add(TEXT("-1")); + Parameters.Add(TEXT("--format=\"%H %s\"")); + bResults = RunCommandInternal(TEXT("log"), InPathToGitBinary, InRepositoryRoot, Parameters, TArray(), InfoMessages, ErrorMessages); + if(bResults && InfoMessages.Num() > 0) + { + OutCommitId = InfoMessages[0].Left(40); + OutCommitSummary = InfoMessages[0].RightChop(41); + } + + return bResults; +} + +bool GetRemoteUrl(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutRemoteUrl) +{ + TArray InfoMessages; + TArray ErrorMessages; + TArray Parameters; + Parameters.Add(TEXT("get-url")); + Parameters.Add(TEXT("origin")); + const bool bResults = RunCommandInternal(TEXT("remote"), InPathToGitBinary, InRepositoryRoot, Parameters, TArray(), InfoMessages, ErrorMessages); + if (bResults && InfoMessages.Num() > 0) + { + OutRemoteUrl = InfoMessages[0]; + } + + return bResults; +} + +bool RunCommand(const FString& InCommand, const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages) +{ + bool bResult = true; + + if(InFiles.Num() > GitSourceControlConstants::MaxFilesPerBatch) + { + // Batch files up so we dont exceed command-line limits + int32 FileCount = 0; + while(FileCount < InFiles.Num()) + { + TArray FilesInBatch; + for(int32 FileIndex = 0; FileCount < InFiles.Num() && FileIndex < GitSourceControlConstants::MaxFilesPerBatch; FileIndex++, FileCount++) + { + FilesInBatch.Add(InFiles[FileCount]); + } + + TArray BatchResults; + TArray BatchErrors; + bResult &= RunCommandInternal(InCommand, InPathToGitBinary, InRepositoryRoot, InParameters, FilesInBatch, BatchResults, BatchErrors); + OutResults += BatchResults; + OutErrorMessages += BatchErrors; + } + } + else + { + bResult &= RunCommandInternal(InCommand, InPathToGitBinary, InRepositoryRoot, InParameters, InFiles, OutResults, OutErrorMessages); + } + + return bResult; +} + +// Run a Git "commit" command by batches +bool RunCommit(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages) +{ + bool bResult = true; + + if(InFiles.Num() > GitSourceControlConstants::MaxFilesPerBatch) + { + // Batch files up so we dont exceed command-line limits + int32 FileCount = 0; + { + TArray FilesInBatch; + for(int32 FileIndex = 0; FileIndex < GitSourceControlConstants::MaxFilesPerBatch; FileIndex++, FileCount++) + { + FilesInBatch.Add(InFiles[FileCount]); + } + // First batch is a simple "git commit" command with only the first files + bResult &= RunCommandInternal(TEXT("commit"), InPathToGitBinary, InRepositoryRoot, InParameters, FilesInBatch, OutResults, OutErrorMessages); + } + + TArray Parameters; + for(const auto& Parameter : InParameters) + { + Parameters.Add(Parameter); + } + Parameters.Add(TEXT("--amend")); + + while(FileCount < InFiles.Num()) + { + TArray FilesInBatch; + for(int32 FileIndex = 0; FileCount < InFiles.Num() && FileIndex < GitSourceControlConstants::MaxFilesPerBatch; FileIndex++, FileCount++) + { + FilesInBatch.Add(InFiles[FileCount]); + } + // Next batches "amend" the commit with some more files + TArray BatchResults; + TArray BatchErrors; + bResult &= RunCommandInternal(TEXT("commit"), InPathToGitBinary, InRepositoryRoot, Parameters, FilesInBatch, BatchResults, BatchErrors); + OutResults += BatchResults; + OutErrorMessages += BatchErrors; + } + } + else + { + bResult = RunCommandInternal(TEXT("commit"), InPathToGitBinary, InRepositoryRoot, InParameters, InFiles, OutResults, OutErrorMessages); + } + + return bResult; +} + +/** + * Parse informations on a file locked with Git LFS + * + * Example output of "git lfs locks" +Content\ThirdPersonBP\Blueprints\ThirdPersonCharacter.uasset SRombauts ID:891 +Content\ThirdPersonBP\Blueprints\ThirdPersonGameMode.uasset SRombauts ID:896 + */ +class FGitLfsLocksParser +{ +public: + FGitLfsLocksParser(const FString& InRepositoryRoot, const FString& InStatus, const bool bAbsolutePaths = true) + { + TArray Informations; + InStatus.ParseIntoArray(Informations, TEXT("\t"), true); + if(Informations.Num() >= 3) + { + Informations[0].TrimEndInline(); // Trim whitespace from the end of the filename + Informations[1].TrimEndInline(); // Trim whitespace from the end of the username + if (bAbsolutePaths) + LocalFilename = FPaths::ConvertRelativePathToFull(InRepositoryRoot, Informations[0]); + else + LocalFilename = Informations[0]; + LockUser = MoveTemp(Informations[1]); + } + } + + FString LocalFilename; ///< Filename on disk + FString LockUser; ///< Name of user who has file locked +}; + +/** + * @brief Extract the relative filename from a Git status result. + * + * Examples of status results: +M Content/Textures/T_Perlin_Noise_M.uasset +R Content/Textures/T_Perlin_Noise_M.uasset -> Content/Textures/T_Perlin_Noise_M2.uasset +?? Content/Materials/M_Basic_Wall.uasset +!! BasicCode.sln + * + * @param[in] InResult One line of status + * @return Relative filename extracted from the line of status + * + * @see FGitStatusFileMatcher and StateFromGitStatus() + */ +static FString FilenameFromGitStatus(const FString& InResult) +{ + int32 RenameIndex; + if(InResult.FindLastChar('>', RenameIndex)) + { + // Extract only the second part of a rename "from -> to" + return InResult.RightChop(RenameIndex + 2); + } + else + { + // Extract the relative filename from the Git status result (after the 2 letters status and 1 space) + return InResult.RightChop(3); + } +} + +/** Match the relative filename of a Git status result with a provided absolute filename */ +class FGitStatusFileMatcher +{ +public: + FGitStatusFileMatcher(const FString& InAbsoluteFilename) + : AbsoluteFilename(InAbsoluteFilename) + { + } + + bool operator()(const FString& InResult) const + { + return AbsoluteFilename.Contains(FilenameFromGitStatus(InResult)); + } + +private: + const FString& AbsoluteFilename; +}; + +/** + * Extract and interpret the file state from the given Git status result. + * @see http://git-scm.com/docs/git-status + * ' ' = unmodified + * 'M' = modified + * 'A' = added + * 'D' = deleted + * 'R' = renamed + * 'C' = copied + * 'U' = updated but unmerged + * '?' = unknown/untracked + * '!' = ignored +*/ +class FGitStatusParser +{ +public: + FGitStatusParser(const FString& InResult) + { + TCHAR IndexState = InResult[0]; + TCHAR WCopyState = InResult[1]; + if( (IndexState == 'U' || WCopyState == 'U') + || (IndexState == 'A' && WCopyState == 'A') + || (IndexState == 'D' && WCopyState == 'D')) + { + // "Unmerged" conflict cases are generally marked with a "U", + // but there are also the special cases of both "A"dded, or both "D"eleted + State = EWorkingCopyState::Conflicted; + } + else if(IndexState == 'A') + { + State = EWorkingCopyState::Added; + } + else if(IndexState == 'D') + { + State = EWorkingCopyState::Deleted; + } + else if(WCopyState == 'D') + { + State = EWorkingCopyState::Missing; + } + else if(IndexState == 'M' || WCopyState == 'M') + { + State = EWorkingCopyState::Modified; + } + else if(IndexState == 'R') + { + State = EWorkingCopyState::Renamed; + } + else if(IndexState == 'C') + { + State = EWorkingCopyState::Copied; + } + else if(IndexState == '?' || WCopyState == '?') + { + State = EWorkingCopyState::NotControlled; + } + else if(IndexState == '!' || WCopyState == '!') + { + State = EWorkingCopyState::Ignored; + } + else + { + // Unmodified never yield a status + State = EWorkingCopyState::Unknown; + } + } + + EWorkingCopyState::Type State; +}; + +/** + * Extract the status of a unmerged (conflict) file + * + * Example output of git ls-files --unmerged Content/Blueprints/BP_Test.uasset +100644 d9b33098273547b57c0af314136f35b494e16dcb 1 Content/Blueprints/BP_Test.uasset +100644 a14347dc3b589b78fb19ba62a7e3982f343718bc 2 Content/Blueprints/BP_Test.uasset +100644 f3137a7167c840847cd7bd2bf07eefbfb2d9bcd2 3 Content/Blueprints/BP_Test.uasset + * + * 1: The "common ancestor" of the file (the version of the file that both the current and other branch originated from). + * 2: The version from the current branch (the master branch in this case). + * 3: The version from the other branch (the test branch) +*/ +class FGitConflictStatusParser +{ +public: + /** Parse the unmerge status: extract the base SHA1 identifier of the file */ + FGitConflictStatusParser(const TArray& InResults) + { + const FString& FirstResult = InResults[0]; // 1: The common ancestor of merged branches + CommonAncestorFileId = FirstResult.Mid(7, 40); + } + + FString CommonAncestorFileId; ///< SHA1 Id of the file (warning: not the commit Id) +}; + +/** Execute a command to get the details of a conflict */ +static void RunGetConflictStatus(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InFile, FGitSourceControlState& InOutFileState) +{ + TArray ErrorMessages; + TArray Results; + TArray Files; + Files.Add(InFile); + TArray Parameters; + Parameters.Add(TEXT("--unmerged")); + bool bResult = RunCommandInternal(TEXT("ls-files"), InPathToGitBinary, InRepositoryRoot, Parameters, Files, Results, ErrorMessages); + if(bResult && Results.Num() == 3) + { + // Parse the unmerge status: extract the base revision (or the other branch?) + FGitConflictStatusParser ConflictStatus(Results); + InOutFileState.PendingMergeBaseFileHash = ConflictStatus.CommonAncestorFileId; + } +} + +/// Convert filename relative to the repository root to absolute path (inplace) +void AbsoluteFilenames(const FString& InRepositoryRoot, TArray& InFileNames) +{ + for(auto& FileName : InFileNames) + { + FileName = FPaths::ConvertRelativePathToFull(InRepositoryRoot, FileName); + } +} + +/** Run a 'git ls-files' command to get all files tracked by Git recursively in a directory. + * + * Called in case of a "directory status" (no file listed in the command) when using the "Submit to Source Control" menu. +*/ +static bool ListFilesInDirectoryRecurse(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InDirectory, TArray& OutFiles) +{ + TArray ErrorMessages; + TArray Directory; + Directory.Add(InDirectory); + const bool bResult = RunCommandInternal(TEXT("ls-files"), InPathToGitBinary, InRepositoryRoot, TArray(), Directory, OutFiles, ErrorMessages); + AbsoluteFilenames(InRepositoryRoot, OutFiles); + return bResult; +} + +/** Parse the array of strings results of a 'git status' command for a provided list of files all in a common directory + * + * Called in case of a normal refresh of status on a list of assets in a the Content Browser (or user selected "Refresh" context menu). + * + * Example git status results: +M Content/Textures/T_Perlin_Noise_M.uasset +R Content/Textures/T_Perlin_Noise_M.uasset -> Content/Textures/T_Perlin_Noise_M2.uasset +?? Content/Materials/M_Basic_Wall.uasset +!! BasicCode.sln +*/ +static void ParseFileStatusResult(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool InUsingLfsLocking, const TArray& InFiles, const TMap& InLockedFiles, const TArray& InResults, TArray& OutStates) +{ + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + const FString LfsUserName = GitSourceControl.AccessSettings().GetLfsUserName(); + const FDateTime Now = FDateTime::Now(); + + // Iterate on all files explicitly listed in the command + for(const auto& File : InFiles) + { + FGitSourceControlState FileState(File, InUsingLfsLocking); + // Search the file in the list of status + int32 IdxResult = InResults.IndexOfByPredicate(FGitStatusFileMatcher(File)); + if(IdxResult != INDEX_NONE) + { + // File found in status results; only the case for "changed" files + FGitStatusParser StatusParser(InResults[IdxResult]); + // TODO LFS Debug log + UE_LOG(LogSourceControl, Log, TEXT("Status(%s) = '%s' => %d"), *File, *InResults[IdxResult], static_cast(StatusParser.State)); + + FileState.WorkingCopyState = StatusParser.State; + if(FileState.IsConflicted()) + { + // In case of a conflict (unmerged file) get the base revision to merge + RunGetConflictStatus(InPathToGitBinary, InRepositoryRoot, File, FileState); + } + } + else + { + // File not found in status + if(FPaths::FileExists(File)) + { + // usually means the file is unchanged, + FileState.WorkingCopyState = EWorkingCopyState::Unchanged; + // TODO LFS Debug log + UE_LOG(LogSourceControl, Log, TEXT("Status(%s) not found but exists => unchanged"), *File); + } + else + { + // but also the case for newly created content: there is no file on disk until the content is saved for the first time + FileState.WorkingCopyState = EWorkingCopyState::NotControlled; + // TODO LFS Debug log + UE_LOG(LogSourceControl, Log, TEXT("Status(%s) not found and does not exists => new/not controled"), *File); + } + } + if(InLockedFiles.Contains(File)) + { + FileState.LockUser = InLockedFiles[File]; + if(LfsUserName == FileState.LockUser) + { + FileState.LockState = ELockState::Locked; + } + else + { + FileState.LockState = ELockState::LockedOther; + } + // TODO LFS Debug log + UE_LOG(LogSourceControl, Log, TEXT("Status(%s) Locked by '%s'"), *File, *FileState.LockUser); + } + else + { + FileState.LockState = ELockState::NotLocked; + // TODO LFS Debug log + if (InUsingLfsLocking) + { + UE_LOG(LogSourceControl, Log, TEXT("Status(%s) Not Locked"), *File); + } + } + FileState.TimeStamp = Now; + OutStates.Add(FileState); + } +} + +/** Parse the array of strings results of a 'git status' command for a directory + * + * Called in case of a "directory status" (no file listed in the command) ONLY to detect Deleted/Missing/Untracked files + * since those files are not listed by the 'git ls-files' command. + * + * @see #ParseFileStatusResult() above for an example of a 'git status' results +*/ +static void ParseDirectoryStatusResult(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool InUsingLfsLocking, const TArray& InResults, TArray& OutStates) +{ + // Iterate on each line of result of the status command + for(const FString& Result : InResults) + { + const FString RelativeFilename = FilenameFromGitStatus(Result); + const FString File = FPaths::ConvertRelativePathToFull(InRepositoryRoot, RelativeFilename); + + FGitSourceControlState FileState(File, InUsingLfsLocking); + FGitStatusParser StatusParser(Result); + if((EWorkingCopyState::Deleted == StatusParser.State) || (EWorkingCopyState::Missing == StatusParser.State) || (EWorkingCopyState::NotControlled == StatusParser.State)) + { + FileState.WorkingCopyState = StatusParser.State; + FileState.TimeStamp.Now(); + OutStates.Add(MoveTemp(FileState)); + } + } +} + +/** + * @brief Detects how to parse the result of a "status" command to get workspace file states + * + * It is either a command for a whole directory (ie. "Content/", in case of "Submit to Source Control" menu), + * or for one or more files all on a same directory (by design, since we group files by directory in RunUpdateStatus()) + * + * @param[in] InPathToGitBinary The path to the Git binary + * @param[in] InRepositoryRoot The Git repository from where to run the command - usually the Game directory (can be empty) + * @param[in] InUsingLfsLocking Tells if using the Git LFS file Locking workflow + * @param[in] InFiles List of files in a directory, or the path to the directory itself (never empty). + * @param[out] InResults Results from the "status" command + * @param[out] OutStates States of files for witch the status has been gathered (distinct than InFiles in case of a "directory status") + */ +static void ParseStatusResults(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool InUsingLfsLocking, const TArray& InFiles, const TMap& InLockedFiles, const TArray& InResults, TArray& OutStates) +{ + if((InFiles.Num() == 1) && FPaths::DirectoryExists(InFiles[0])) + { + // 1) Special case for "status" of a directory: requires to get the list of files by ourselves. + // (this is triggered by the "Submit to Source Control" menu) + // TODO LFS Debug Log + UE_LOG(LogSourceControl, Log, TEXT("ParseStatusResults: 1) Special case for status of a directory (%s)"), *InFiles[0]); + TArray Files; + const FString& Directory = InFiles[0]; + const bool bResult = ListFilesInDirectoryRecurse(InPathToGitBinary, InRepositoryRoot, Directory, Files); + if(bResult) + { + ParseFileStatusResult(InPathToGitBinary, InRepositoryRoot, InUsingLfsLocking, Files, InLockedFiles, InResults, OutStates); + } + // The above cannot detect deleted assets since there is no file left to enumerate (either by the Content Browser or by git ls-files) + // => so we also parse the status results to explicitly look for Deleted/Missing assets + ParseDirectoryStatusResult(InPathToGitBinary, InRepositoryRoot, InUsingLfsLocking, InResults, OutStates); + } + else + { + // 2) General case for one or more files in the same directory. + // TODO LFS Debug Log + UE_LOG(LogSourceControl, Log, TEXT("ParseStatusResults: 2) General case for one or more files (%s, ...)"), *InFiles[0]); + ParseFileStatusResult(InPathToGitBinary, InRepositoryRoot, InUsingLfsLocking, InFiles, InLockedFiles, InResults, OutStates); + } +} + +bool GetAllLocks(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool bAbsolutePaths, TArray& OutErrorMessages, TMap& OutLocks) +{ + TArray Results; + TArray ErrorMessages; + const bool bResult = RunCommand(TEXT("lfs locks"), InPathToGitBinary, InRepositoryRoot, TArray(), TArray(), Results, ErrorMessages); + for(const FString& Result : Results) + { + FGitLfsLocksParser LockFile(InRepositoryRoot, Result, bAbsolutePaths); + // TODO LFS Debug log + UE_LOG(LogSourceControl, Log, TEXT("LockedFile(%s, %s)"), *LockFile.LocalFilename, *LockFile.LockUser); + OutLocks.Add(MoveTemp(LockFile.LocalFilename), MoveTemp(LockFile.LockUser)); + } + + return bResult; +} + +// Run a batch of Git "status" command to update status of given files and/or directories. +bool RunUpdateStatus(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool InUsingLfsLocking, const TArray& InFiles, TArray& OutErrorMessages, TArray& OutStates) +{ + bool bResults = true; + TMap LockedFiles; + + // 0) Issue a "git lfs locks" command at the root of the repository + if(InUsingLfsLocking) + { + TArray ErrorMessages; + GetAllLocks(InPathToGitBinary, InRepositoryRoot, true, ErrorMessages, LockedFiles); + } + + // Git status does not show any "untracked files" when called with files from different subdirectories! (issue #3) + // 1) So here we group files by path (ie. by subdirectory) + TMap> GroupOfFiles; + for(const auto& File : InFiles) + { + const FString Path = FPaths::GetPath(*File); + TArray* Group = GroupOfFiles.Find(Path); + if(Group != nullptr) + { + Group->Add(File); + } + else + { + TArray NewGroup; + NewGroup.Add(File); + GroupOfFiles.Add(Path, NewGroup); + } + } + + // Get the current branch name, since we need origin of current branch + FString BranchName; + GitSourceControlUtils::GetBranchName(InPathToGitBinary, InRepositoryRoot, BranchName); + + TArray Parameters; + Parameters.Add(TEXT("--porcelain")); + Parameters.Add(TEXT("--ignored")); + + // 2) then we can batch git status operation by subdirectory + for(const auto& Files : GroupOfFiles) + { + // "git status" can only detect renamed and deleted files when it operate on a folder, so use one folder path for all files in a directory + const FString Path = FPaths::GetPath(*Files.Value[0]); + TArray OnePath; + // Only one file: optim very useful for the .uproject file at the root to avoid parsing the whole repository + // (works only if the file exists) + if((Files.Value.Num() == 1) && (FPaths::FileExists(Files.Value[0]))) + { + OnePath.Add(Files.Value[0]); + } + else + { + OnePath.Add(Path); + } + { + TArray Results; + TArray ErrorMessages; + const bool bResult = RunCommand(TEXT("status"), InPathToGitBinary, InRepositoryRoot, Parameters, OnePath, Results, ErrorMessages); + OutErrorMessages.Append(ErrorMessages); + if(bResult) + { + ParseStatusResults(InPathToGitBinary, InRepositoryRoot, InUsingLfsLocking, Files.Value, LockedFiles, Results, OutStates); + } + } + + if (!BranchName.IsEmpty()) + { + // Using git diff, we can obtain a list of files that were modified between our current origin and HEAD. Assumes that fetch has been run to get accurate info. + // TODO: should do a fetch (at least periodically). + TArray Results; + TArray ErrorMessages; + TArray ParametersLsRemote; + ParametersLsRemote.Add(TEXT("origin")); + ParametersLsRemote.Add(BranchName); + const bool bResultLsRemote = RunCommand(TEXT("ls-remote"), InPathToGitBinary, InRepositoryRoot, ParametersLsRemote, OnePath, Results, ErrorMessages); + // If the command is successful and there is only 1 line on the output the branch exists on remote + const bool bDiffAgainstRemote = bResultLsRemote && Results.Num(); + + Results.Reset(); + ErrorMessages.Reset(); + TArray ParametersLog; + ParametersLog.Add(TEXT("--pretty=")); // this omits the commit lines, just gets us files + ParametersLog.Add(TEXT("--name-only")); + ParametersLog.Add(bDiffAgainstRemote ? TEXT("HEAD..HEAD@{upstream}") : BranchName); + const bool bResultDiff = RunCommand(TEXT("log"), InPathToGitBinary, InRepositoryRoot, ParametersLog, OnePath, Results, ErrorMessages); + OutErrorMessages.Append(ErrorMessages); + if (bResultDiff) + { + for (const FString& NewerFileName : Results) + { + const FString NewerFilePath = FPaths::ConvertRelativePathToFull(InRepositoryRoot, NewerFileName); + + // Find existing corresponding file state to update it (not found would mean new file or not in the current path) + if (FGitSourceControlState* FileStatePtr = OutStates.FindByPredicate([NewerFilePath](FGitSourceControlState& FileState) { return FileState.LocalFilename == NewerFilePath; })) + { + FileStatePtr->bNewerVersionOnServer = true; + } + } + } + } + } + + return bResults; +} + +// Run a Git `cat-file --filters` command to dump the binary content of a revision into a file. +bool RunDumpToFile(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InParameter, const FString& InDumpFileName) +{ + int32 ReturnCode = -1; + FString FullCommand; + + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + const FGitVersion& GitVersion = GitSourceControl.GetProvider().GetGitVersion(); + + if(!InRepositoryRoot.IsEmpty()) + { + // Specify the working copy (the root) of the git repository (before the command itself) + FullCommand = TEXT("-C \""); + FullCommand += InRepositoryRoot; + FullCommand += TEXT("\" "); + } + + // then the git command itself + if(GitVersion.bHasCatFileWithFilters) + { + // Newer versions (2.9.3.windows.2) support smudge/clean filters used by Git LFS, git-fat, git-annex, etc + FullCommand += TEXT("cat-file --filters "); + } + else + { + // Previous versions fall-back on "git show" like before + FullCommand += TEXT("show "); + } + + // Append to the command the parameter + FullCommand += InParameter; + + const bool bLaunchDetached = false; + const bool bLaunchHidden = true; + const bool bLaunchReallyHidden = bLaunchHidden; + + void* PipeRead = nullptr; + void* PipeWrite = nullptr; + + verify(FPlatformProcess::CreatePipe(PipeRead, PipeWrite)); + + UE_LOG(LogSourceControl, Log, TEXT("RunDumpToFile: 'git %s'"), *FullCommand); + + FString PathToGitOrEnvBinary = InPathToGitBinary; + #if PLATFORM_MAC + // The Cocoa application does not inherit shell environment variables, so add the path expected to have git-lfs to PATH + FString PathEnv = FPlatformMisc::GetEnvironmentVariable(TEXT("PATH")); + FString GitInstallPath = FPaths::GetPath(InPathToGitBinary); + + TArray PathArray; + PathEnv.ParseIntoArray(PathArray, FPlatformMisc::GetPathVarDelimiter()); + bool bHasGitInstallPath = false; + for (auto Path : PathArray) + { + if (GitInstallPath.Equals(Path, ESearchCase::CaseSensitive)) + { + bHasGitInstallPath = true; + break; + } + } + + if (!bHasGitInstallPath) + { + PathToGitOrEnvBinary = FString("/usr/bin/env"); + FullCommand = FString::Printf(TEXT("PATH=\"%s%s%s\" \"%s\" %s"), *GitInstallPath, FPlatformMisc::GetPathVarDelimiter(), *PathEnv, *InPathToGitBinary, *FullCommand); + } + #endif + + FProcHandle ProcessHandle = FPlatformProcess::CreateProc(*PathToGitOrEnvBinary, *FullCommand, bLaunchDetached, bLaunchHidden, bLaunchReallyHidden, nullptr, 0, *InRepositoryRoot, PipeWrite); + if(ProcessHandle.IsValid()) + { + FPlatformProcess::Sleep(0.01); + + TArray BinaryFileContent; + while(FPlatformProcess::IsProcRunning(ProcessHandle)) + { + TArray BinaryData; + FPlatformProcess::ReadPipeToArray(PipeRead, BinaryData); + if(BinaryData.Num() > 0) + { + BinaryFileContent.Append(MoveTemp(BinaryData)); + } + } + TArray BinaryData; + FPlatformProcess::ReadPipeToArray(PipeRead, BinaryData); + if(BinaryData.Num() > 0) + { + BinaryFileContent.Append(MoveTemp(BinaryData)); + } + + FPlatformProcess::GetProcReturnCode(ProcessHandle, &ReturnCode); + if(ReturnCode == 0) + { + // Save buffer into temp file + if(FFileHelper::SaveArrayToFile(BinaryFileContent, *InDumpFileName)) + { + UE_LOG(LogSourceControl, Log, TEXT("Writed '%s' (%do)"), *InDumpFileName, BinaryFileContent.Num()); + } + else + { + UE_LOG(LogSourceControl, Error, TEXT("Could not write %s"), *InDumpFileName); + ReturnCode = -1; + } + } + else + { + UE_LOG(LogSourceControl, Error, TEXT("DumpToFile: ReturnCode=%d"), ReturnCode); + } + + FPlatformProcess::CloseProc(ProcessHandle); + } + else + { + UE_LOG(LogSourceControl, Error, TEXT("Failed to launch 'git cat-file'")); + } + + FPlatformProcess::ClosePipe(PipeRead, PipeWrite); + + return (ReturnCode == 0); +} + +/** + * Translate file actions from the given Git log --name-status command to keywords used by the Editor UI. + * + * @see https://www.kernel.org/pub/software/scm/git/docs/git-log.html + * ' ' = unmodified + * 'M' = modified + * 'A' = added + * 'D' = deleted + * 'R' = renamed + * 'C' = copied + * 'T' = type changed + * 'U' = updated but unmerged + * 'X' = unknown + * 'B' = broken pairing + * + * @see SHistoryRevisionListRowContent::GenerateWidgetForColumn(): "add", "edit", "delete", "branch" and "integrate" (everything else is taken like "edit") +*/ +static FString LogStatusToString(TCHAR InStatus) +{ + switch(InStatus) + { + case TEXT(' '): + return FString("unmodified"); + case TEXT('M'): + return FString("modified"); + case TEXT('A'): // added: keyword "add" to display a specific icon instead of the default "edit" action one + return FString("add"); + case TEXT('D'): // deleted: keyword "delete" to display a specific icon instead of the default "edit" action one + return FString("delete"); + case TEXT('R'): // renamed keyword "branch" to display a specific icon instead of the default "edit" action one + return FString("branch"); + case TEXT('C'): // copied keyword "branch" to display a specific icon instead of the default "edit" action one + return FString("branch"); + case TEXT('T'): + return FString("type changed"); + case TEXT('U'): + return FString("unmerged"); + case TEXT('X'): + return FString("unknown"); + case TEXT('B'): + return FString("broked pairing"); + } + + return FString(); +} + +/** + * Parse the array of strings results of a 'git log' command + * + * Example git log results: +commit 97a4e7626681895e073aaefd68b8ac087db81b0b +Author: Sébastien Rombauts +Date: 2014-2015-05-15 21:32:27 +0200 + + Another commit used to test History + + - with many lines + - some + - and strange characteres $*+ + +M Content/Blueprints/Blueprint_CeilingLight.uasset +R100 Content/Textures/T_Concrete_Poured_D.uasset Content/Textures/T_Concrete_Poured_D2.uasset + +commit 355f0df26ebd3888adbb558fd42bb8bd3e565000 +Author: Sébastien Rombauts +Date: 2014-2015-05-12 11:28:14 +0200 + + Testing git status, edit, and revert + +A Content/Blueprints/Blueprint_CeilingLight.uasset +C099 Content/Textures/T_Concrete_Poured_N.uasset Content/Textures/T_Concrete_Poured_N2.uasset +*/ +static void ParseLogResults(const TArray& InResults, TGitSourceControlHistory& OutHistory) +{ + TSharedRef SourceControlRevision = MakeShareable(new FGitSourceControlRevision); + for(const auto& Result : InResults) + { + if(Result.StartsWith(TEXT("commit "))) // Start of a new commit + { + // End of the previous commit + if(SourceControlRevision->RevisionNumber != 0) + { + OutHistory.Add(MoveTemp(SourceControlRevision)); + + SourceControlRevision = MakeShareable(new FGitSourceControlRevision); + } + SourceControlRevision->CommitId = Result.RightChop(7); // Full commit SHA1 hexadecimal string + SourceControlRevision->ShortCommitId = SourceControlRevision->CommitId.Left(8); // Short revision ; first 8 hex characters (max that can hold a 32 bit integer) + SourceControlRevision->CommitIdNumber = FParse::HexNumber(*SourceControlRevision->ShortCommitId); + SourceControlRevision->RevisionNumber = -1; // RevisionNumber will be set at the end, based off the index in the History + } + else if(Result.StartsWith(TEXT("Author: "))) // Author name & email + { + // Remove the 'email' part of the UserName + FString UserNameEmail = Result.RightChop(8); + int32 EmailIndex = 0; + if(UserNameEmail.FindLastChar('<', EmailIndex)) + { + SourceControlRevision->UserName = UserNameEmail.Left(EmailIndex - 1); + } + } + else if(Result.StartsWith(TEXT("Date: "))) // Commit date + { + FString Date = Result.RightChop(8); + SourceControlRevision->Date = FDateTime::FromUnixTimestamp(FCString::Atoi(*Date)); + } + // else if(Result.IsEmpty()) // empty line before/after commit message has already been taken care by FString::ParseIntoArray() + else if(Result.StartsWith(TEXT(" "))) // Multi-lines commit message + { + SourceControlRevision->Description += Result.RightChop(4); + SourceControlRevision->Description += TEXT("\n"); + } + else // Name of the file, starting with an uppercase status letter ("A"/"M"...) + { + const TCHAR Status = Result[0]; + SourceControlRevision->Action = LogStatusToString(Status); // Readable action string ("Added", Modified"...) instead of "A"/"M"... + // Take care of special case for Renamed/Copied file: extract the second filename after second tabulation + int32 IdxTab; + if(Result.FindLastChar('\t', IdxTab)) + { + SourceControlRevision->Filename = Result.RightChop(IdxTab + 1); // relative filename + } + } + } + // End of the last commit + if(SourceControlRevision->RevisionNumber != 0) + { + OutHistory.Add(MoveTemp(SourceControlRevision)); + } + + // Then set the revision number of each Revision based on its index (reverse order since the log starts with the most recent change) + for(int32 RevisionIndex = 0; RevisionIndex < OutHistory.Num(); RevisionIndex++) + { + const auto& SourceControlRevisionItem = OutHistory[RevisionIndex]; + SourceControlRevisionItem->RevisionNumber = OutHistory.Num() - RevisionIndex; + + // Special case of a move ("branch" in Perforce term): point to the previous change (so the next one in the order of the log) + if((SourceControlRevisionItem->Action == "branch") && (RevisionIndex < OutHistory.Num() - 1)) + { + SourceControlRevisionItem->BranchSource = OutHistory[RevisionIndex + 1]; + } + } +} + +/** + * Extract the SHA1 identifier and size of a blob (file) from a Git "ls-tree" command. + * + * Example output for the command git ls-tree --long 7fdaeb2 Content/Blueprints/BP_Test.uasset +100644 blob a14347dc3b589b78fb19ba62a7e3982f343718bc 70731 Content/Blueprints/BP_Test.uasset +*/ +class FGitLsTreeParser +{ +public: + /** Parse the unmerge status: extract the base SHA1 identifier of the file */ + FGitLsTreeParser(const TArray& InResults) + { + const FString& FirstResult = InResults[0]; + FileHash = FirstResult.Mid(12, 40); + int32 IdxTab; + if(FirstResult.FindChar('\t', IdxTab)) + { + const FString SizeString = FirstResult.Mid(53, IdxTab - 53); + FileSize = FCString::Atoi(*SizeString); + } + } + + FString FileHash; ///< SHA1 Id of the file (warning: not the commit Id) + int32 FileSize; ///< Size of the file (in bytes) +}; + +// Run a Git "log" command and parse it. +bool RunGetHistory(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InFile, bool bMergeConflict, TArray& OutErrorMessages, TGitSourceControlHistory& OutHistory) +{ + bool bResults; + { + TArray Results; + TArray Parameters; + Parameters.Add(TEXT("--follow")); // follow file renames + Parameters.Add(TEXT("--date=raw")); + Parameters.Add(TEXT("--name-status")); // relative filename at this revision, preceded by a status character + Parameters.Add(TEXT("--pretty=medium")); // make sure format matches expected in ParseLogResults + if(bMergeConflict) + { + // In case of a merge conflict, we also need to get the tip of the "remote branch" (MERGE_HEAD) before the log of the "current branch" (HEAD) + // @todo does not work for a cherry-pick! Test for a rebase. + Parameters.Add(TEXT("MERGE_HEAD")); + Parameters.Add(TEXT("--max-count 1")); + } + TArray Files; + Files.Add(*InFile); + bResults = RunCommand(TEXT("log"), InPathToGitBinary, InRepositoryRoot, Parameters, Files, Results, OutErrorMessages); + if(bResults) + { + ParseLogResults(Results, OutHistory); + } + } + for(auto& Revision : OutHistory) + { + // Get file (blob) sha1 id and size + TArray Results; + TArray Parameters; + Parameters.Add(TEXT("--long")); // Show object size of blob (file) entries. + Parameters.Add(Revision->GetRevision()); + TArray Files; + Files.Add(*Revision->GetFilename()); + bResults &= RunCommand(TEXT("ls-tree"), InPathToGitBinary, InRepositoryRoot, Parameters, Files, Results, OutErrorMessages); + if(bResults && Results.Num()) + { + FGitLsTreeParser LsTree(Results); + Revision->FileHash = LsTree.FileHash; + Revision->FileSize = LsTree.FileSize; + } + } + + return bResults; +} + +TArray RelativeFilenames(const TArray& InFileNames, const FString& InRelativeTo) +{ + TArray RelativeFiles; + FString RelativeTo = InRelativeTo; + + // Ensure that the path ends w/ '/' + if((RelativeTo.Len() > 0) && (RelativeTo.EndsWith(TEXT("/"), ESearchCase::CaseSensitive) == false) && (RelativeTo.EndsWith(TEXT("\\"), ESearchCase::CaseSensitive) == false)) + { + RelativeTo += TEXT("/"); + } + for(FString FileName : InFileNames) // string copy to be able to convert it inplace + { + if(FPaths::MakePathRelativeTo(FileName, *RelativeTo)) + { + RelativeFiles.Add(FileName); + } + } + + return RelativeFiles; +} + +TArray AbsoluteFilenames(const TArray& InFileNames, const FString& InRelativeTo) +{ + TArray AbsFiles; + + for(FString FileName : InFileNames) // string copy to be able to convert it inplace + { + AbsFiles.Add(FPaths::Combine(InRelativeTo, FileName)); + } + + return AbsFiles; +} + +bool UpdateCachedStates(const TArray& InStates) +{ + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked( "GitSourceControl" ); + FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); + const bool bUsingGitLfsLocking = GitSourceControl.AccessSettings().IsUsingGitLfsLocking(); + + // TODO without LFS : Workaround a bug with the Source Control Module not updating file state after a simple "Save" with no "Checkout" (when not using File Lock) + const FDateTime Now = bUsingGitLfsLocking ? FDateTime::Now() : FDateTime(); + + for(const auto& InState : InStates) + { + TSharedRef State = Provider.GetStateInternal(InState.LocalFilename); + *State = InState; + State->TimeStamp = Now; + } + + return (InStates.Num() > 0); +} + +/** + * Helper struct for RemoveRedundantErrors() + */ +struct FRemoveRedundantErrors +{ + FRemoveRedundantErrors(const FString& InFilter) + : Filter(InFilter) + { + } + + bool operator()(const FString& String) const + { + if(String.Contains(Filter)) + { + return true; + } + + return false; + } + + /** The filter string we try to identify in the reported error */ + FString Filter; +}; + +void RemoveRedundantErrors(FGitSourceControlCommand& InCommand, const FString& InFilter) +{ + bool bFoundRedundantError = false; + for(auto Iter(InCommand.ErrorMessages.CreateConstIterator()); Iter; Iter++) + { + if(Iter->Contains(InFilter)) + { + InCommand.InfoMessages.Add(*Iter); + bFoundRedundantError = true; + } + } + + InCommand.ErrorMessages.RemoveAll( FRemoveRedundantErrors(InFilter) ); + + // if we have no error messages now, assume success! + if(bFoundRedundantError && InCommand.ErrorMessages.Num() == 0 && !InCommand.bCommandSuccessful) + { + InCommand.bCommandSuccessful = true; + } +} + +} diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlUtils.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlUtils.h new file mode 100644 index 0000000..971b433 --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlUtils.h @@ -0,0 +1,220 @@ +// 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 "GitSourceControlState.h" + +class FGitSourceControlCommand; + +/** + * Helper struct for maintaining temporary files for passing to commands + */ +class FGitScopedTempFile +{ +public: + + /** Constructor - open & write string to temp file */ + FGitScopedTempFile(const FText& InText); + + /** Destructor - delete temp file */ + ~FGitScopedTempFile(); + + /** Get the filename of this temp file - empty if it failed to be created */ + const FString& GetFilename() const; + +private: + /** The filename we are writing to */ + FString Filename; +}; + +struct FGitVersion; + +namespace GitSourceControlUtils +{ + +/** + * Find the path to the Git binary, looking into a few places (standalone Git install, and other common tools embedding Git) + * @returns the path to the Git binary if found, or an empty string. + */ +FString FindGitBinaryPath(); + +/** + * Run a Git "version" command to check the availability of the binary. + * @param InPathToGitBinary The path to the Git binary + * @param OutGitVersion If provided, populate with the git version parsed from "version" command + * @returns true if the command succeeded and returned no errors + */ +bool CheckGitAvailability(const FString& InPathToGitBinary, FGitVersion* OutVersion = nullptr); + +/** + * Parse the output from the "version" command into GitMajorVersion and GitMinorVersion. + * @param InVersionString The version string returned by `git --version` + * @param OutVersion The FGitVersion to populate + */ + void ParseGitVersion(const FString& InVersionString, FGitVersion* OutVersion); + +/** + * Check git for various optional capabilities by various means. + * @param InPathToGitBinary The path to the Git binary + * @param OutGitVersion If provided, populate with the git version parsed from "version" command + */ +void FindGitCapabilities(const FString& InPathToGitBinary, FGitVersion *OutVersion); + +/** + * Run a Git "lfs" command to check the availability of the "Large File System" extension. + * @param InPathToGitBinary The path to the Git binary + * @param OutGitVersion If provided, populate with the git version parsed from "version" command + */ + void FindGitLfsCapabilities(const FString& InPathToGitBinary, FGitVersion *OutVersion); + +/** + * Find the root of the Git repository, looking from the provided path and upward in its parent directories + * @param InPath The path to the Game Directory (or any path or file in any git repository) + * @param OutRepositoryRoot The path to the root directory of the Git repository if found, else the path to the ProjectDir + * @returns true if the command succeeded and returned no errors + */ +bool FindRootDirectory(const FString& InPath, FString& OutRepositoryRoot); + +/** + * Get Git config user.name & user.email + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory (can be empty) + * @param OutUserName Name of the Git user configured for this repository (or globaly) + * @param OutEmailName E-mail of the Git user configured for this repository (or globaly) + */ +void GetUserConfig(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutUserName, FString& OutUserEmail); + +/** + * Get Git current checked-out branch + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory + * @param OutBranchName Name of the current checked-out branch (if any, ie. not in detached HEAD) + * @returns true if the command succeeded and returned no errors + */ +bool GetBranchName(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutBranchName); + +/** + * Get Git current commit details + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory + * @param OutCommitId Current Commit full SHA1 + * @param OutCommitSummary Current Commit description's Summary + * @returns true if the command succeeded and returned no errors + */ +bool GetCommitInfo(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutCommitId, FString& OutCommitSummary); + +/** + * Get the URL of the "origin" defaut remote server + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory + * @param OutRemoteUrl URL of "origin" defaut remote server + * @returns true if the command succeeded and returned no errors + */ +bool GetRemoteUrl(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutRemoteUrl); + +/** + * Run a Git command - output is a string TArray. + * + * @param InCommand The Git command - e.g. commit + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory (can be empty) + * @param InParameters The parameters to the Git command + * @param InFiles The files to be operated on + * @param OutResults The results (from StdOut) as an array per-line + * @param OutErrorMessages Any errors (from StdErr) as an array per-line + * @returns true if the command succeeded and returned no errors + */ +bool RunCommand(const FString& InCommand, const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages); + +/** + * Run a Git "commit" command by batches. + * + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory + * @param InParameter The parameters to the Git commit command + * @param InFiles The files to be operated on + * @param OutErrorMessages Any errors (from StdErr) as an array per-line + * @returns true if the command succeeded and returned no errors + */ +bool RunCommit(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages); + +/** + * Run a Git "status" command and parse it. + * + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory (can be empty) + * @param InUsingLfsLocking Tells if using the Git LFS file Locking workflow + * @param InFiles The files to be operated on + * @param OutErrorMessages Any errors (from StdErr) as an array per-line + * @returns true if the command succeeded and returned no errors + */ +bool RunUpdateStatus(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool InUsingLfsLocking, const TArray& InFiles, TArray& OutErrorMessages, TArray& OutStates); + +/** + * Run a Git "cat-file" command to dump the binary content of a revision into a file. + * + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory + * @param InParameter The parameters to the Git show command (rev:path) + * @param InDumpFileName The temporary file to dump the revision + * @returns true if the command succeeded and returned no errors +*/ +bool RunDumpToFile(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InParameter, const FString& InDumpFileName); + +/** + * Run a Git "log" command and parse it. + * + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory + * @param InFile The file to be operated on + * @param bMergeConflict In case of a merge conflict, we also need to get the tip of the "remote branch" (MERGE_HEAD) before the log of the "current branch" (HEAD) + * @param OutErrorMessages Any errors (from StdErr) as an array per-line + * @param OutHistory The history of the file + */ +bool RunGetHistory(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InFile, bool bMergeConflict, TArray& OutErrorMessages, TGitSourceControlHistory& OutHistory); + +/** + * Helper function to convert a filename array to relative paths. + * @param InFileNames The filename array + * @param InRelativeTo Path to the WorkspaceRoot + * @return an array of filenames, transformed into relative paths + */ +TArray RelativeFilenames(const TArray& InFileNames, const FString& InRelativeTo); + +/** + * Helper function to convert a filename array to absolute paths. + * @param InFileNames The filename array (relative paths) + * @param InRelativeTo Path to the WorkspaceRoot + * @return an array of filenames, transformed into absolute paths + */ +TArray AbsoluteFilenames(const TArray& InFileNames, const FString& InRelativeTo); + +/** + * Helper function for various commands to update cached states. + * @returns true if any states were updated + */ +bool UpdateCachedStates(const TArray& InStates); + +/** + * Remove redundant errors (that contain a particular string) and also + * update the commands success status if all errors were removed. + */ +void RemoveRedundantErrors(FGitSourceControlCommand& InCommand, const FString& InFilter); + +/** + * Run 'git lfs locks" to extract all lock information for all files in the repository + * + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory + * @param bAbsolutePaths Whether to report absolute filenames, false for repo-relative + * @param OutErrorMessages Any errors (from StdErr) as an array per-line + * @param OutLocks The lock results (file, username) + * @returns true if the command succeeded and returned no errors + */ +bool GetAllLocks(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool bAbsolutePaths, TArray& OutErrorMessages, TMap& OutLocks); + +} diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/IGitSourceControlWorker.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/IGitSourceControlWorker.h new file mode 100644 index 0000000..fb2a01a --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/IGitSourceControlWorker.h @@ -0,0 +1,30 @@ +// 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 IGitSourceControlWorker +{ +public: + /** + * Name describing the work that this worker does. Used for factory method hookup. + */ + virtual FName GetName() const = 0; + + /** + * Function that actually does the work. Can be executed on another thread. + */ + virtual bool Execute( class FGitSourceControlCommand& InCommand ) = 0; + + /** + * Updates the state of any items after completion (if necessary). This is always executed on the main thread. + * @returns true if states were updated + */ + virtual bool UpdateStates() const = 0; +}; + +typedef TSharedRef FGitSourceControlWorkerRef; diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp new file mode 100644 index 0000000..ce1d9ab --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp @@ -0,0 +1,750 @@ +// 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 "SGitSourceControlSettings.h" + +#include "Fonts/SlateFontInfo.h" +#include "Misc/App.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "Modules/ModuleManager.h" +#include "Styling/SlateTypes.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/Text/STextBlock.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/Input/SEditableTextBox.h" +#include "Widgets/Input/SFilePathPicker.h" +#include "Widgets/Input/SMultiLineEditableTextBox.h" +#include "Widgets/Layout/SBorder.h" +#include "Widgets/Layout/SSeparator.h" +#include "Widgets/Notifications/SNotificationList.h" +#include "Framework/Notifications/NotificationManager.h" +#include "EditorDirectories.h" +#include "EditorStyleSet.h" +#include "SourceControlOperations.h" +#include "GitSourceControlModule.h" +#include "GitSourceControlUtils.h" + +#define LOCTEXT_NAMESPACE "SGitSourceControlSettings" + +void SGitSourceControlSettings::Construct(const FArguments& InArgs) +{ + const FSlateFontInfo Font = FEditorStyle::GetFontStyle(TEXT("SourceControl.LoginWindow.Font")); + + bAutoCreateGitIgnore = true; + bAutoCreateReadme = true; + bAutoCreateGitAttributes = false; + bAutoInitialCommit = true; + + InitialCommitMessage = LOCTEXT("InitialCommitMessage", "Initial commit"); + + const FText FileFilterType = NSLOCTEXT("GitSourceControl", "Executables", "Executables"); +#if PLATFORM_WINDOWS + const FString FileFilterText = FString::Printf(TEXT("%s (*.exe)|*.exe"), *FileFilterType.ToString()); +#else + const FString FileFilterText = FString::Printf(TEXT("%s"), *FileFilterType.ToString()); +#endif + + ReadmeContent = FText::FromString(FString(TEXT("# ")) + FApp::GetProjectName() + "\n\nDeveloped with Unreal Engine 4\n"); + + ChildSlot + [ + SNew(SBorder) + .BorderImage( FEditorStyle::GetBrush("DetailsView.CategoryBottom")) + .Padding(FMargin(0.0f, 3.0f, 0.0f, 0.0f)) + [ + SNew(SVerticalBox) + // Path to the Git command line executable + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .ToolTipText(LOCTEXT("BinaryPathLabel_Tooltip", "Path to Git binary")) + +SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("BinaryPathLabel", "Git Path")) + .Font(Font) + ] + +SHorizontalBox::Slot() + .FillWidth(2.0f) + [ + SNew(SFilePathPicker) + .BrowseButtonImage(FEditorStyle::GetBrush("PropertyWindow.Button_Ellipsis")) + .BrowseButtonStyle(FEditorStyle::Get(), "HoverHintOnly") + .BrowseDirectory(FEditorDirectories::Get().GetLastDirectory(ELastDirectory::GENERIC_OPEN)) + .BrowseTitle(LOCTEXT("BinaryPathBrowseTitle", "File picker...")) + .FilePath(this, &SGitSourceControlSettings::GetBinaryPathString) + .FileTypeFilter(FileFilterText) + .OnPathPicked(this, &SGitSourceControlSettings::OnBinaryPathPicked) + ] + ] + // Root of the local repository + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .ToolTipText(LOCTEXT("RepositoryRootLabel_Tooltip", "Path to the root of the Git repository")) + +SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("RepositoryRootLabel", "Root of the repository")) + .Font(Font) + ] + +SHorizontalBox::Slot() + .FillWidth(2.0f) + [ + SNew(STextBlock) + .Text(this, &SGitSourceControlSettings::GetPathToRepositoryRoot) + .Font(Font) + ] + ] + // User Name + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .ToolTipText(LOCTEXT("GitUserName_Tooltip", "User name configured for the Git repository")) + +SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("GitUserName", "User Name")) + .Font(Font) + ] + +SHorizontalBox::Slot() + .FillWidth(2.0f) + [ + SNew(STextBlock) + .Text(this, &SGitSourceControlSettings::GetUserName) + .Font(Font) + ] + ] + // User e-mail + +SVerticalBox::Slot() + .FillHeight(1.0f) + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .ToolTipText(LOCTEXT("GitUserEmail_Tooltip", "User e-mail configured for the Git repository")) + +SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("GitUserEmail", "E-Mail")) + .Font(Font) + ] + +SHorizontalBox::Slot() + .FillWidth(2.0f) + [ + SNew(STextBlock) + .Text(this, &SGitSourceControlSettings::GetUserEmail) + .Font(Font) + ] + ] + // Separator + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SSeparator) + ] + // Explanation text + +SVerticalBox::Slot() + .FillHeight(1.0f) + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) + +SHorizontalBox::Slot() + .FillWidth(1.0f) + .HAlign(HAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("RepositoryNotFound", "Current Project is not contained in a Git Repository. Fill the form below to initialize a new Repository.")) + .ToolTipText(LOCTEXT("RepositoryNotFound_Tooltip", "No Repository found at the level or above the current Project")) + .Font(Font) + ] + ] + // Option to configure the URL of the default remote 'origin' + // TODO: option to configure the name of the remote instead of the default origin + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) + .ToolTipText(LOCTEXT("ConfigureOrigin_Tooltip", "Configure the URL of the default remote 'origin'")) + +SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("ConfigureOrigin", "URL of the remote server 'origin'")) + .Font(Font) + ] + +SHorizontalBox::Slot() + .FillWidth(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SEditableTextBox) + .Text(this, &SGitSourceControlSettings::GetRemoteUrl) + .OnTextCommitted(this, &SGitSourceControlSettings::OnRemoteUrlCommited) + .Font(Font) + ] + ] + // Option to add a proper .gitignore file (true by default) + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) + .ToolTipText(LOCTEXT("CreateGitIgnore_Tooltip", "Create and add a standard '.gitignore' file")) + +SHorizontalBox::Slot() + .FillWidth(0.1f) + [ + SNew(SCheckBox) + .IsChecked(ECheckBoxState::Checked) + .OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedCreateGitIgnore) + ] + +SHorizontalBox::Slot() + .FillWidth(2.9f) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("CreateGitIgnore", "Add a .gitignore file")) + .Font(Font) + ] + ] + // Option to add a README.md file with custom content + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) + .ToolTipText(LOCTEXT("CreateReadme_Tooltip", "Add a README.md file")) + +SHorizontalBox::Slot() + .FillWidth(0.1f) + [ + SNew(SCheckBox) + .IsChecked(ECheckBoxState::Checked) + .OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedCreateReadme) + ] + +SHorizontalBox::Slot() + .FillWidth(0.9f) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("CreateReadme", "Add a basic README.md file")) + .Font(Font) + ] + +SHorizontalBox::Slot() + .FillWidth(2.0f) + .Padding(2.0f) + [ + SNew(SMultiLineEditableTextBox) + .Text(this, &SGitSourceControlSettings::GetReadmeContent) + .OnTextCommitted(this, &SGitSourceControlSettings::OnReadmeContentCommited) + .IsEnabled(this, &SGitSourceControlSettings::GetAutoCreateReadme) + .SelectAllTextWhenFocused(true) + .Font(Font) + ] + ] + // Option to add a proper .gitattributes file for Git LFS (false by default) + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) + .ToolTipText(LOCTEXT("CreateGitAttributes_Tooltip", "Create and add a '.gitattributes' file to enable Git LFS for the whole 'Content/' directory (needs Git LFS extensions to be installed).")) + +SHorizontalBox::Slot() + .FillWidth(0.1f) + [ + SNew(SCheckBox) + .IsChecked(ECheckBoxState::Unchecked) + .OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedCreateGitAttributes) + .IsEnabled(this, &SGitSourceControlSettings::CanInitializeGitLfs) + ] + +SHorizontalBox::Slot() + .FillWidth(2.9f) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("CreateGitAttributes", "Add a .gitattributes file to enable Git LFS")) + .Font(Font) + ] + ] + // Option to use the Git LFS File Locking workflow (false by default) + // Enabled even after init to switch it off in case of no network + // TODO LFS turning it off afterwards does not work because all files are readonly ! + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .ToolTipText(LOCTEXT("UseGitLfsLocking_Tooltip", "Uses Git LFS 2 File Locking workflow (CheckOut and Commit/Push).")) + +SHorizontalBox::Slot() + .FillWidth(0.1f) + [ + SNew(SCheckBox) + .IsChecked(SGitSourceControlSettings::IsUsingGitLfsLocking()) + .OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedUseGitLfsLocking) + .IsEnabled(this, &SGitSourceControlSettings::CanUseGitLfsLocking) + ] + +SHorizontalBox::Slot() + .FillWidth(0.9f) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("UseGitLfsLocking", "Uses Git LFS 2 File Locking workflow")) + .Font(Font) + ] + // Username credential used to access the Git LFS 2 File Locks server + +SHorizontalBox::Slot() + .FillWidth(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SEditableTextBox) + .Text(this, &SGitSourceControlSettings::GetLfsUserName) + .OnTextCommitted(this, &SGitSourceControlSettings::OnLfsUserNameCommited) + .IsEnabled(this, &SGitSourceControlSettings::GetIsUsingGitLfsLocking) + .HintText(LOCTEXT("LfsUserName_Hint", "Username to lock files on the LFS server")) + .Font(Font) + ] + ] + // Option to Make the initial Git commit with custom message + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) + .ToolTipText(LOCTEXT("InitialGitCommit_Tooltip", "Make the initial Git commit")) + +SHorizontalBox::Slot() + .FillWidth(0.1f) + [ + SNew(SCheckBox) + .IsChecked(ECheckBoxState::Checked) + .OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedInitialCommit) + ] + +SHorizontalBox::Slot() + .FillWidth(0.9f) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("InitialGitCommit", "Make the initial Git commit")) + .Font(Font) + ] + +SHorizontalBox::Slot() + .FillWidth(2.0f) + .Padding(2.0f) + [ + SNew(SMultiLineEditableTextBox) + .Text(this, &SGitSourceControlSettings::GetInitialCommitMessage) + .OnTextCommitted(this, &SGitSourceControlSettings::OnInitialCommitMessageCommited) + .IsEnabled(this, &SGitSourceControlSettings::GetAutoInitialCommit) + .SelectAllTextWhenFocused(true) + .Font(Font) + ] + ] + // Button to initialize the project with Git, create .gitignore/.gitattributes files, and make the first commit) + +SVerticalBox::Slot() + .FillHeight(2.5f) + .Padding(4.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) + +SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SButton) + .Text(LOCTEXT("GitInitRepository", "Initialize project with Git")) + .ToolTipText(LOCTEXT("GitInitRepository_Tooltip", "Initialize current project as a new Git repository")) + .OnClicked(this, &SGitSourceControlSettings::OnClickedInitializeGitRepository) + .IsEnabled(this, &SGitSourceControlSettings::CanInitializeGitRepository) + .HAlign(HAlign_Center) + .ContentPadding(6) + ] + ] + ] + ]; +} + +SGitSourceControlSettings::~SGitSourceControlSettings() +{ + RemoveInProgressNotification(); +} + +FString SGitSourceControlSettings::GetBinaryPathString() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + return GitSourceControl.AccessSettings().GetBinaryPath(); +} + +void SGitSourceControlSettings::OnBinaryPathPicked( const FString& PickedPath ) const +{ + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + FString PickedFullPath = FPaths::ConvertRelativePathToFull(PickedPath); + const bool bChanged = GitSourceControl.AccessSettings().SetBinaryPath(PickedFullPath); + if(bChanged) + { + // Re-Check provided git binary path for each change + GitSourceControl.GetProvider().CheckGitAvailability(); + if(GitSourceControl.GetProvider().IsGitAvailable()) + { + GitSourceControl.SaveSettings(); + } + } +} + +FText SGitSourceControlSettings::GetPathToRepositoryRoot() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + return FText::FromString(GitSourceControl.GetProvider().GetPathToRepositoryRoot()); +} + +FText SGitSourceControlSettings::GetUserName() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + return FText::FromString(GitSourceControl.GetProvider().GetUserName()); +} + +FText SGitSourceControlSettings::GetUserEmail() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + return FText::FromString(GitSourceControl.GetProvider().GetUserEmail()); +} + +EVisibility SGitSourceControlSettings::MustInitializeGitRepository() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + const bool bGitAvailable = GitSourceControl.GetProvider().IsGitAvailable(); + const bool bGitRepositoryFound = GitSourceControl.GetProvider().IsEnabled(); + return (bGitAvailable && !bGitRepositoryFound) ? EVisibility::Visible : EVisibility::Collapsed; +} + +bool SGitSourceControlSettings::CanInitializeGitRepository() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + const bool bGitAvailable = GitSourceControl.GetProvider().IsGitAvailable(); + const bool bGitRepositoryFound = GitSourceControl.GetProvider().IsEnabled(); + const FString LfsUserName = GitSourceControl.AccessSettings().GetLfsUserName(); + const bool bIsUsingGitLfsLocking = GitSourceControl.AccessSettings().IsUsingGitLfsLocking(); + const bool bGitLfsConfigOk = !bIsUsingGitLfsLocking || !LfsUserName.IsEmpty(); + const bool bInitialCommitConfigOk = !bAutoInitialCommit || !InitialCommitMessage.IsEmpty(); + return (bGitAvailable && !bGitRepositoryFound && bGitLfsConfigOk && bInitialCommitConfigOk); +} + +bool SGitSourceControlSettings::CanInitializeGitLfs() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath(); + const bool bGitLfsAvailable = GitSourceControl.GetProvider().GetGitVersion().bHasGitLfs; + return bGitLfsAvailable; +} + +bool SGitSourceControlSettings::CanUseGitLfsLocking() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + const bool bGitLfsLockingAvailable = GitSourceControl.GetProvider().GetGitVersion().bHasGitLfsLocking; + // TODO LFS SRombauts : check if .gitattributes file is present and if Content/ is already tracked! + const bool bGitAttributesCreated = true; + return (bGitLfsLockingAvailable && (bAutoCreateGitAttributes || bGitAttributesCreated)); +} + +FReply SGitSourceControlSettings::OnClickedInitializeGitRepository() +{ + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath(); + const FString PathToProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir()); + TArray InfoMessages; + TArray ErrorMessages; + + // 1.a. Synchronous (very quick) "git init" operation: initialize a Git local repository with a .git/ subdirectory + GitSourceControlUtils::RunCommand(TEXT("init"), PathToGitBinary, PathToProjectDir, TArray(), TArray(), InfoMessages, ErrorMessages); + // 1.b. Synchronous (very quick) "git remote add" operation: configure the URL of the default remote server 'origin' if specified + if(!RemoteUrl.IsEmpty()) + { + TArray Parameters; + Parameters.Add(TEXT("add origin")); + Parameters.Add(RemoteUrl.ToString()); + GitSourceControlUtils::RunCommand(TEXT("remote"), PathToGitBinary, PathToProjectDir, Parameters, TArray(), InfoMessages, ErrorMessages); + } + + // Check the new repository status to enable connection (branch, user e-mail) + GitSourceControl.GetProvider().CheckRepositoryStatus(PathToGitBinary); + if(GitSourceControl.GetProvider().IsAvailable()) + { + // List of files to add to Source Control (.uproject, Config/, Content/, Source/ files and .gitignore/.gitattributes if any) + TArray ProjectFiles; + ProjectFiles.Add(FPaths::GetProjectFilePath()); + ProjectFiles.Add(FPaths::ProjectConfigDir()); + ProjectFiles.Add(FPaths::ProjectContentDir()); + if (FPaths::DirectoryExists(FPaths::GameSourceDir())) + { + ProjectFiles.Add(FPaths::GameSourceDir()); + } + if(bAutoCreateGitIgnore) + { + // 2.a. Create a standard ".gitignore" file with common patterns for a typical Blueprint & C++ project + const FString GitIgnoreFilename = FPaths::Combine(FPaths::ProjectDir(), TEXT(".gitignore")); + const FString GitIgnoreContent = TEXT("Binaries\nDerivedDataCache\nIntermediate\nSaved\n.vscode\n.vs\n*.VC.db\n*.opensdf\n*.opendb\n*.sdf\n*.sln\n*.suo\n*.xcodeproj\n*.xcworkspace\n*.log"); + if(FFileHelper::SaveStringToFile(GitIgnoreContent, *GitIgnoreFilename, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) + { + ProjectFiles.Add(GitIgnoreFilename); + } + } + if(bAutoCreateReadme) + { + // 2.b. Create a "README.md" file with a custom description + const FString ReadmeFilename = FPaths::Combine(FPaths::ProjectDir(), TEXT("README.md")); + if (FFileHelper::SaveStringToFile(ReadmeContent.ToString(), *ReadmeFilename, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) + { + ProjectFiles.Add(ReadmeFilename); + } + } + if(bAutoCreateGitAttributes) + { + // 2.c. Synchronous (very quick) "lfs install" operation: needs only to be run once by user + GitSourceControlUtils::RunCommand(TEXT("lfs install"), PathToGitBinary, PathToProjectDir, TArray(), TArray(), InfoMessages, ErrorMessages); + + // 2.d. Create a ".gitattributes" file to enable Git LFS (Large File System) for the whole "Content/" subdir + const FString GitAttributesFilename = FPaths::Combine(FPaths::ProjectDir(), TEXT(".gitattributes")); + FString GitAttributesContent; + if(GitSourceControl.AccessSettings().IsUsingGitLfsLocking()) + { + // Git LFS 2.x File Locking mechanism + GitAttributesContent = TEXT("Content/** filter=lfs diff=lfs merge=lfs -text lockable\n"); + } + else + { + GitAttributesContent = TEXT("Content/** filter=lfs diff=lfs merge=lfs -text\n"); + } + if(FFileHelper::SaveStringToFile(GitAttributesContent, *GitAttributesFilename, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) + { + ProjectFiles.Add(GitAttributesFilename); + } + } + + // 3. Add files to Source Control: launch an asynchronous MarkForAdd operation + LaunchMarkForAddOperation(ProjectFiles); + + // 4. The CheckIn will follow, at completion of the MarkForAdd operation + } + return FReply::Handled(); +} + +// Launch an asynchronous "MarkForAdd" operation and start an ongoing notification +void SGitSourceControlSettings::LaunchMarkForAddOperation(const TArray& InFiles) +{ + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + TSharedRef MarkForAddOperation = ISourceControlOperation::Create(); + ECommandResult::Type Result = GitSourceControl.GetProvider().Execute(MarkForAddOperation, InFiles, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateSP(this, &SGitSourceControlSettings::OnSourceControlOperationComplete)); + if (Result == ECommandResult::Succeeded) + { + DisplayInProgressNotification(MarkForAddOperation); + } + else + { + DisplayFailureNotification(MarkForAddOperation); + } +} + +// Launch an asynchronous "CheckIn" operation and start another ongoing notification +void SGitSourceControlSettings::LaunchCheckInOperation() +{ + TSharedRef CheckInOperation = ISourceControlOperation::Create(); + CheckInOperation->SetDescription(InitialCommitMessage); + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + ECommandResult::Type Result = GitSourceControl.GetProvider().Execute(CheckInOperation, TArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateSP(this, &SGitSourceControlSettings::OnSourceControlOperationComplete)); + if (Result == ECommandResult::Succeeded) + { + DisplayInProgressNotification(CheckInOperation); + } + else + { + DisplayFailureNotification(CheckInOperation); + } +} + +/// Delegate called when a source control operation has completed: launch the next one and manage notifications +void SGitSourceControlSettings::OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult) +{ + RemoveInProgressNotification(); + + // Report result with a notification + if (InResult == ECommandResult::Succeeded) + { + DisplaySuccessNotification(InOperation); + } + else + { + DisplayFailureNotification(InOperation); + } + + if ((InOperation->GetName() == "MarkForAdd") && (InResult == ECommandResult::Succeeded) && bAutoInitialCommit) + { + // 4. optional initial Asynchronous commit with custom message: launch a "CheckIn" Operation + LaunchCheckInOperation(); + } +} + + +// Display an ongoing notification during the whole operation +void SGitSourceControlSettings::DisplayInProgressNotification(const FSourceControlOperationRef& InOperation) +{ + FNotificationInfo Info(InOperation->GetInProgressString()); + 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 SGitSourceControlSettings::RemoveInProgressNotification() +{ + if (OperationInProgressNotification.IsValid()) + { + OperationInProgressNotification.Pin()->ExpireAndFadeout(); + OperationInProgressNotification.Reset(); + } +} + +// Display a temporary success notification at the end of the operation +void SGitSourceControlSettings::DisplaySuccessNotification(const FSourceControlOperationRef& InOperation) +{ + const FText NotificationText = FText::Format(LOCTEXT("InitialCommit_Success", "{0} operation was successfull!"), FText::FromName(InOperation->GetName())); + FNotificationInfo Info(NotificationText); + Info.bUseSuccessFailIcons = true; + Info.Image = FEditorStyle::GetBrush(TEXT("NotificationList.SuccessImage")); + FSlateNotificationManager::Get().AddNotification(Info); +} + +// Display a temporary failure notification at the end of the operation +void SGitSourceControlSettings::DisplayFailureNotification(const FSourceControlOperationRef& InOperation) +{ + const FText NotificationText = FText::Format(LOCTEXT("InitialCommit_Failure", "Error: {0} operation failed!"), FText::FromName(InOperation->GetName())); + FNotificationInfo Info(NotificationText); + Info.ExpireDuration = 8.0f; + FSlateNotificationManager::Get().AddNotification(Info); +} + +void SGitSourceControlSettings::OnCheckedCreateGitIgnore(ECheckBoxState NewCheckedState) +{ + bAutoCreateGitIgnore = (NewCheckedState == ECheckBoxState::Checked); +} + +void SGitSourceControlSettings::OnCheckedCreateReadme(ECheckBoxState NewCheckedState) +{ + bAutoCreateReadme = (NewCheckedState == ECheckBoxState::Checked); +} + +bool SGitSourceControlSettings::GetAutoCreateReadme() const +{ + return bAutoCreateReadme; +} + +void SGitSourceControlSettings::OnReadmeContentCommited(const FText& InText, ETextCommit::Type InCommitType) +{ + ReadmeContent = InText; +} + +FText SGitSourceControlSettings::GetReadmeContent() const +{ + return ReadmeContent; +} + +void SGitSourceControlSettings::OnCheckedCreateGitAttributes(ECheckBoxState NewCheckedState) +{ + bAutoCreateGitAttributes = (NewCheckedState == ECheckBoxState::Checked); +} + +void SGitSourceControlSettings::OnCheckedUseGitLfsLocking(ECheckBoxState NewCheckedState) +{ + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + GitSourceControl.AccessSettings().SetUsingGitLfsLocking(NewCheckedState == ECheckBoxState::Checked); + GitSourceControl.AccessSettings().SaveSettings(); +} + +bool SGitSourceControlSettings::GetIsUsingGitLfsLocking() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + return GitSourceControl.AccessSettings().IsUsingGitLfsLocking(); +} + +ECheckBoxState SGitSourceControlSettings::IsUsingGitLfsLocking() const +{ + return (GetIsUsingGitLfsLocking() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked); +} + +void SGitSourceControlSettings::OnLfsUserNameCommited(const FText& InText, ETextCommit::Type InCommitType) +{ + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + GitSourceControl.AccessSettings().SetLfsUserName(InText.ToString()); + GitSourceControl.AccessSettings().SaveSettings(); +} + +FText SGitSourceControlSettings::GetLfsUserName() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + return FText::FromString(GitSourceControl.AccessSettings().GetLfsUserName()); +} + +void SGitSourceControlSettings::OnCheckedInitialCommit(ECheckBoxState NewCheckedState) +{ + bAutoInitialCommit = (NewCheckedState == ECheckBoxState::Checked); +} + +bool SGitSourceControlSettings::GetAutoInitialCommit() const +{ + return bAutoInitialCommit; +} + +void SGitSourceControlSettings::OnInitialCommitMessageCommited(const FText& InText, ETextCommit::Type InCommitType) +{ + InitialCommitMessage = InText; +} + +FText SGitSourceControlSettings::GetInitialCommitMessage() const +{ + return InitialCommitMessage; +} + +void SGitSourceControlSettings::OnRemoteUrlCommited(const FText& InText, ETextCommit::Type InCommitType) +{ + RemoteUrl = InText; +} + +FText SGitSourceControlSettings::GetRemoteUrl() const +{ + return RemoteUrl; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/SGitSourceControlSettings.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/SGitSourceControlSettings.h new file mode 100644 index 0000000..38d4855 --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/SGitSourceControlSettings.h @@ -0,0 +1,98 @@ +// 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 "Layout/Visibility.h" +#include "Input/Reply.h" +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/SCompoundWidget.h" +#include "SlateFwd.h" +#include "ISourceControlOperation.h" +#include "ISourceControlProvider.h" + +enum class ECheckBoxState : uint8; + +class SGitSourceControlSettings : public SCompoundWidget +{ +public: + + SLATE_BEGIN_ARGS(SGitSourceControlSettings) {} + + SLATE_END_ARGS() + +public: + + void Construct(const FArguments& InArgs); + + ~SGitSourceControlSettings(); + +private: + + /** Delegates to get Git binary path from/to settings */ + FString GetBinaryPathString() const; + void OnBinaryPathPicked(const FString & PickedPath) const; + + /** Delegate to get repository root, user name and email from provider */ + FText GetPathToRepositoryRoot() const; + FText GetUserName() const; + FText GetUserEmail() const; + + EVisibility MustInitializeGitRepository() const; + bool CanInitializeGitRepository() const; + bool CanInitializeGitLfs() const; + bool CanUseGitLfsLocking() const; + + /** Delegate to initialize a new Git repository */ + FReply OnClickedInitializeGitRepository(); + + void OnCheckedCreateGitIgnore(ECheckBoxState NewCheckedState); + bool bAutoCreateGitIgnore; + + /** Delegates to create a README.md file */ + void OnCheckedCreateReadme(ECheckBoxState NewCheckedState); + bool GetAutoCreateReadme() const; + bool bAutoCreateReadme; + void OnReadmeContentCommited(const FText& InText, ETextCommit::Type InCommitType); + FText GetReadmeContent() const; + FText ReadmeContent; + + void OnCheckedCreateGitAttributes(ECheckBoxState NewCheckedState); + bool bAutoCreateGitAttributes; + + void OnCheckedUseGitLfsLocking(ECheckBoxState NewCheckedState); + ECheckBoxState IsUsingGitLfsLocking() const; + bool GetIsUsingGitLfsLocking() const; + + void OnLfsUserNameCommited(const FText& InText, ETextCommit::Type InCommitType); + FText GetLfsUserName() const; + + void OnCheckedInitialCommit(ECheckBoxState NewCheckedState); + bool GetAutoInitialCommit() const; + bool bAutoInitialCommit; + void OnInitialCommitMessageCommited(const FText& InText, ETextCommit::Type InCommitType); + FText GetInitialCommitMessage() const; + FText InitialCommitMessage; + + void OnRemoteUrlCommited(const FText& InText, ETextCommit::Type InCommitType); + FText GetRemoteUrl() const; + FText RemoteUrl; + + /** Launch initial asynchronous add and commit operations */ + void LaunchMarkForAddOperation(const TArray& InFiles); + void LaunchCheckInOperation(); + + /** Delegate called when a source control operation has completed */ + void OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult); + + /** Asynchronous operation progress notifications */ + TWeakPtr OperationInProgressNotification; + + void DisplayInProgressNotification(const FSourceControlOperationRef& InOperation); + void RemoveInProgressNotification(); + void DisplaySuccessNotification(const FSourceControlOperationRef& InOperation); + void DisplayFailureNotification(const FSourceControlOperationRef& InOperation); +}; diff --git a/Plugins/UE4GitPlugin-2.17-beta/_config.yml b/Plugins/UE4GitPlugin-2.17-beta/_config.yml new file mode 100644 index 0000000..f170406 --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/_config.yml @@ -0,0 +1,2 @@ +show_downloads: true +theme: jekyll-theme-slate \ No newline at end of file