ReAdded Git
7
Plugins/UE4GitPlugin-2.17-beta/.gitbugtraq
Normal file
@ -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+
|
5
Plugins/UE4GitPlugin-2.17-beta/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/Binaries/*/*.pdb
|
||||
/Binaries/*/*Debug*
|
||||
/Binaries/*/*.dylib
|
||||
/Binaries/*/*.modules
|
||||
/Intermediate
|
25
Plugins/UE4GitPlugin-2.17-beta/GitSourceControl.uplugin
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
20
Plugins/UE4GitPlugin-2.17-beta/LICENSE.txt
Normal file
@ -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.
|
201
Plugins/UE4GitPlugin-2.17-beta/README.md
Normal file
@ -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)
|
||||
<a href="https://www.paypal.me/SRombauts" title="Pay Me a Beer! Donate with PayPal :)"><img src="https://www.paypalobjects.com/webstatic/paypalme/images/pp_logo_small.png" width="118"></a>
|
||||
|
||||
- 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:
|
||||
<img src="https://cdn2.unrealengine.com/blog/DiffTool-1009x542-719850393.png" width="720">
|
||||
|
||||
Merge conflict of a Blueprint:
|
||||
<img src="https://docs.unrealengine.com/latest/images/Support/Builds/ReleaseNotes/2015/4_7/BPmergeTool.jpg" width="720">
|
||||
|
||||
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):
|
||||
|
||||
```
|
||||
<YourGameProject>/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:
|
||||
|
||||
<img src="Screenshots/SourceControlLogin_Init.png" width="720">
|
||||
|
||||
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)
|
BIN
Plugins/UE4GitPlugin-2.17-beta/Resources/Icon128.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
Plugins/UE4GitPlugin-2.17-beta/Screenshots/FileHistory.png
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Added.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Modified.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/New.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Renamed.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Unchanged.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 62 KiB |
BIN
Plugins/UE4GitPlugin-2.17-beta/Screenshots/SourceControlMenu.png
Normal file
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 28 KiB |
BIN
Plugins/UE4GitPlugin-2.17-beta/Screenshots/SubmitFiles.png
Normal file
After Width: | Height: | Size: 42 KiB |
@ -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",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -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<class ISourceControlOperation, ESPMode::ThreadSafe>& InOperation, const TSharedRef<class IGitSourceControlWorker, ESPMode::ThreadSafe>& 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<FGitSourceControlModule>( "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;
|
||||
}
|
@ -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<class ISourceControlOperation, ESPMode::ThreadSafe>& InOperation, const TSharedRef<class IGitSourceControlWorker, ESPMode::ThreadSafe>& 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<class ISourceControlOperation, ESPMode::ThreadSafe> Operation;
|
||||
|
||||
/** The object that will actually do the work */
|
||||
TSharedRef<class IGitSourceControlWorker, ESPMode::ThreadSafe> 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<FString> Files;
|
||||
|
||||
/**Info and/or warning message storage*/
|
||||
TArray<FString> InfoMessages;
|
||||
|
||||
/**Potential error message storage*/
|
||||
TArray<FString> ErrorMessages;
|
||||
};
|
@ -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<FLevelEditorModule>("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<FLevelEditorModule>("LevelEditor");
|
||||
if (LevelEditorModule)
|
||||
{
|
||||
LevelEditorModule->GetAllLevelEditorToolbarSourceControlMenuExtenders().RemoveAll([=](const FLevelEditorModule::FLevelEditorMenuExtender& Extender) { return Extender.GetHandle() == ViewMenuExtenderHandle; });
|
||||
}
|
||||
}
|
||||
|
||||
bool FGitSourceControlMenu::HaveRemoteUrl() const
|
||||
{
|
||||
const FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked<FGitSourceControlModule>("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<UPackage*> DirtyPackages;
|
||||
FEditorFileUtils::GetDirtyWorldPackages(DirtyPackages);
|
||||
FEditorFileUtils::GetDirtyContentPackages(DirtyPackages);
|
||||
bSaved = DirtyPackages.Num() == 0;
|
||||
}
|
||||
|
||||
return bSaved;
|
||||
}
|
||||
|
||||
/// Find all packages in Content directory
|
||||
TArray<FString> FGitSourceControlMenu::ListAllPackages()
|
||||
{
|
||||
TArray<FString> PackageRelativePaths;
|
||||
FPackageName::FindPackagesInDirectory(PackageRelativePaths, *FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()));
|
||||
|
||||
TArray<FString> 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<UPackage*> FGitSourceControlMenu::UnlinkPackages(const TArray<FString>& InPackageNames)
|
||||
{
|
||||
TArray<UPackage*> 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<UPackage*>& 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<UPackage*> 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<FGitSourceControlModule>("GitSourceControl");
|
||||
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
|
||||
const FString& PathToRespositoryRoot = Provider.GetPathToRepositoryRoot();
|
||||
const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
|
||||
const TArray<FString> ParametersStatus{"--porcelain --untracked-files=no"};
|
||||
TArray<FString> InfoMessages;
|
||||
TArray<FString> ErrorMessages;
|
||||
// Check if there is any modification to the working tree
|
||||
const bool bStatusOk = GitSourceControlUtils::RunCommand(TEXT("status"), PathToGitBinary, PathToRespositoryRoot, ParametersStatus, TArray<FString>(), 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<FString> ParametersStash{ "save \"Stashed by Unreal Engine Git Plugin\"" };
|
||||
bStashMadeBeforeSync = GitSourceControlUtils::RunCommand(TEXT("stash"), PathToGitBinary, PathToRespositoryRoot, ParametersStash, TArray<FString>(), 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<FGitSourceControlModule>("GitSourceControl");
|
||||
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
|
||||
const FString& PathToRespositoryRoot = Provider.GetPathToRepositoryRoot();
|
||||
const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
|
||||
const TArray<FString> ParametersStash{ "pop" };
|
||||
TArray<FString> InfoMessages;
|
||||
TArray<FString> ErrorMessages;
|
||||
const bool bUnstashOk = GitSourceControlUtils::RunCommand(TEXT("stash"), PathToGitBinary, PathToRespositoryRoot, ParametersStash, TArray<FString>(), 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<FGitSourceControlModule>("GitSourceControl");
|
||||
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
|
||||
TSharedRef<FSync, ESPMode::ThreadSafe> SyncOperation = ISourceControlOperation::Create<FSync>();
|
||||
const ECommandResult::Type Result = Provider.Execute(SyncOperation, TArray<FString>(), 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<FGitSourceControlModule>("GitSourceControl");
|
||||
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
|
||||
TSharedRef<FGitPush, ESPMode::ThreadSafe> PushOperation = ISourceControlOperation::Create<FGitPush>();
|
||||
const ECommandResult::Type Result = Provider.Execute(PushOperation, TArray<FString>(), 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<FGitSourceControlModule>("GitSourceControl");
|
||||
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
|
||||
TSharedRef<FRevert, ESPMode::ThreadSafe> RevertOperation = ISourceControlOperation::Create<FRevert>();
|
||||
const ECommandResult::Type Result = Provider.Execute(RevertOperation, TArray<FString>(), 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<FGitSourceControlModule>("GitSourceControl");
|
||||
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
|
||||
TSharedRef<FUpdateStatus, ESPMode::ThreadSafe> RefreshOperation = ISourceControlOperation::Create<FUpdateStatus>();
|
||||
RefreshOperation->SetCheckingAllFiles(true);
|
||||
const ECommandResult::Type Result = Provider.Execute(RefreshOperation, TArray<FString>(), 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<FExtender> FGitSourceControlMenu::OnExtendLevelEditorViewMenu(const TSharedRef<FUICommandList> CommandList)
|
||||
{
|
||||
TSharedRef<FExtender> Extender(new FExtender());
|
||||
|
||||
Extender->AddMenuExtension(
|
||||
"SourceControlActions",
|
||||
EExtensionHook::After,
|
||||
nullptr,
|
||||
FMenuExtensionDelegate::CreateRaw(this, &FGitSourceControlMenu::AddMenuExtension));
|
||||
|
||||
return Extender;
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
@ -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<FString> ListAllPackages();
|
||||
TArray<UPackage*> UnlinkPackages(const TArray<FString>& InPackageNames);
|
||||
void ReloadPackages(TArray<UPackage*>& InPackagesToReload);
|
||||
|
||||
bool StashAwayAnyModifications();
|
||||
void ReApplyStashedModifications();
|
||||
|
||||
void AddMenuExtension(FMenuBuilder& Builder);
|
||||
|
||||
TSharedRef<class FExtender> OnExtendLevelEditorViewMenu(const TSharedRef<class FUICommandList> 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<UPackage*> PackagesToReload;
|
||||
|
||||
/** Current source control operation from extended menu if any */
|
||||
TWeakPtr<class SNotificationItem> OperationInProgressNotification;
|
||||
|
||||
/** Delegate called when a source control operation has completed */
|
||||
void OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult);
|
||||
};
|
@ -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<typename Type>
|
||||
static TSharedRef<IGitSourceControlWorker, ESPMode::ThreadSafe> 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<FGitConnectWorker> ) );
|
||||
// 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<FGitCheckOutWorker> ) );
|
||||
GitSourceControlProvider.RegisterWorker( "UpdateStatus", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitUpdateStatusWorker> ) );
|
||||
GitSourceControlProvider.RegisterWorker( "MarkForAdd", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitMarkForAddWorker> ) );
|
||||
GitSourceControlProvider.RegisterWorker( "Delete", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitDeleteWorker> ) );
|
||||
GitSourceControlProvider.RegisterWorker( "Revert", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitRevertWorker> ) );
|
||||
GitSourceControlProvider.RegisterWorker( "Sync", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitSyncWorker> ) );
|
||||
GitSourceControlProvider.RegisterWorker( "Push", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitPushWorker> ) );
|
||||
GitSourceControlProvider.RegisterWorker( "CheckIn", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitCheckInWorker> ) );
|
||||
GitSourceControlProvider.RegisterWorker( "Copy", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitCopyWorker> ) );
|
||||
GitSourceControlProvider.RegisterWorker( "Resolve", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitResolveWorker> ) );
|
||||
|
||||
// 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
|
@ -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;
|
||||
};
|
@ -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<FConnect, ESPMode::ThreadSafe> Operation = StaticCastSharedRef<FConnect>(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<FString> 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<FString>(), TArray<FString>(), 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<FString> RelativeFiles = GitSourceControlUtils::RelativeFilenames(InCommand.Files, InCommand.PathToRepositoryRoot);
|
||||
for(const auto& RelativeFile : RelativeFiles)
|
||||
{
|
||||
TArray<FString> OneFile;
|
||||
OneFile.Add(RelativeFile);
|
||||
InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("lfs lock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray<FString>(), 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<FString>& 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<FString> GetLockedFiles(const TArray<FString>& InFiles)
|
||||
{
|
||||
TArray<FString> LockedFiles;
|
||||
|
||||
FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("GitSourceControl");
|
||||
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
|
||||
|
||||
TArray<TSharedRef<ISourceControlState, ESPMode::ThreadSafe>> 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<FCheckIn, ESPMode::ThreadSafe> Operation = StaticCastSharedRef<FCheckIn>(InCommand.Operation);
|
||||
|
||||
// make a temp file to place our commit message in
|
||||
FGitScopedTempFile CommitMsgFile(Operation->GetDescription());
|
||||
if(CommitMsgFile.GetFilename().Len() > 0)
|
||||
{
|
||||
TArray<FString> 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<FGitSourceControlModule>("GitSourceControl");
|
||||
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
|
||||
|
||||
TArray<TSharedRef<ISourceControlState, ESPMode::ThreadSafe>> 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<FString> Parameters2;
|
||||
// TODO Configure origin
|
||||
Parameters2.Add(TEXT("origin"));
|
||||
Parameters2.Add(TEXT("HEAD"));
|
||||
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("push"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters2, TArray<FString>(), 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<FString> ParametersStatus{"--porcelain --untracked-files=no"};
|
||||
TArray<FString> StatusInfoMessages;
|
||||
TArray<FString> StatusErrorMessages;
|
||||
// Check if there is any modification to the working tree
|
||||
const bool bStatusOk = GitSourceControlUtils::RunCommand(TEXT("status"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, ParametersStatus, TArray<FString>(), StatusInfoMessages, StatusErrorMessages);
|
||||
if ((bStatusOk) && (StatusInfoMessages.Num() > 0))
|
||||
{
|
||||
bStashNeeded = true;
|
||||
const TArray<FString> ParametersStash{ "save \"Stashed by Unreal Engine Git Plugin\"" };
|
||||
bStashed = GitSourceControlUtils::RunCommand(TEXT("stash"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, ParametersStash, TArray<FString>(), 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<FString>(), TArray<FString>(), InCommand.InfoMessages, InCommand.ErrorMessages);
|
||||
if (InCommand.bCommandSuccessful)
|
||||
{
|
||||
// Repeat the push
|
||||
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("push origin HEAD"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray<FString>(), TArray<FString>(), InCommand.InfoMessages, InCommand.ErrorMessages);
|
||||
}
|
||||
|
||||
// Succeed or fail, restore the stash
|
||||
if (bStashed)
|
||||
{
|
||||
const TArray<FString> ParametersStashPop{ "pop" };
|
||||
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("stash"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, ParametersStashPop, TArray<FString>(), 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<FString> LockedFiles = GetLockedFiles(InCommand.Files);
|
||||
if(LockedFiles.Num() > 0)
|
||||
{
|
||||
const TArray<FString> RelativeFiles = GitSourceControlUtils::RelativeFilenames(LockedFiles, InCommand.PathToRepositoryRoot);
|
||||
for(const auto& RelativeFile : RelativeFiles)
|
||||
{
|
||||
TArray<FString> OneFile;
|
||||
OneFile.Add(RelativeFile);
|
||||
GitSourceControlUtils::RunCommand(TEXT("lfs unlock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray<FString>(), 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<FString>(), 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<FString>(), 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<FString>& InFiles, TArray<FString>& OutMissingFiles, TArray<FString>& OutAllExistingFiles, TArray<FString>& OutOtherThanAddedExistingFiles)
|
||||
{
|
||||
FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("GitSourceControl");
|
||||
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
|
||||
|
||||
const TArray<FString> Files = (InFiles.Num() > 0) ? (InFiles) : (Provider.GetFilesInCache());
|
||||
|
||||
TArray<TSharedRef<ISourceControlState, ESPMode::ThreadSafe>> 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<FString> MissingFiles;
|
||||
TArray<FString> AllExistingFiles;
|
||||
TArray<FString> 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<FString>(), 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<FString>(), 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<FString>(), 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<FString> LockedFiles = GetLockedFiles(OtherThanAddedExistingFiles);
|
||||
if(LockedFiles.Num() > 0)
|
||||
{
|
||||
const TArray<FString> RelativeFiles = GitSourceControlUtils::RelativeFilenames(LockedFiles, InCommand.PathToRepositoryRoot);
|
||||
for(const auto& RelativeFile : RelativeFiles)
|
||||
{
|
||||
TArray<FString> OneFile;
|
||||
OneFile.Add(RelativeFile);
|
||||
GitSourceControlUtils::RunCommand(TEXT("lfs unlock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray<FString>(), 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<FString> 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<FString> 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<FString>(), 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<FString> FilesToUnlock;
|
||||
if (InCommand.bUsingGitLfsLocking)
|
||||
{
|
||||
TMap<FString, FString> 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<FString> LfsPushParameters;
|
||||
LfsPushParameters.Add(TEXT("push"));
|
||||
LfsPushParameters.Add(TEXT("--dry-run"));
|
||||
LfsPushParameters.Add(TEXT("origin"));
|
||||
LfsPushParameters.Add(BranchName);
|
||||
TArray<FString> LfsPushInfoMessages;
|
||||
TArray<FString> LfsPushErrMessages;
|
||||
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("lfs"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, LfsPushParameters, TArray<FString>(), 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<FString> 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<FString>(), 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<FString> OneFile;
|
||||
OneFile.Add(FileToUnlock);
|
||||
bool bUnlocked = GitSourceControlUtils::RunCommand(TEXT("lfs unlock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray<FString>(), 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<FString> 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<FUpdateStatus, ESPMode::ThreadSafe> Operation = StaticCastSharedRef<FUpdateStatus>(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<FString> 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<FGitSourceControlModule>( "GitSourceControl" );
|
||||
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
|
||||
|
||||
const FDateTime Now = FDateTime::Now();
|
||||
|
||||
// add history, if any
|
||||
for(const auto& History : Histories)
|
||||
{
|
||||
TSharedRef<FGitSourceControlState, ESPMode::ThreadSafe> 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<FString>(), 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<FString> Results;
|
||||
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray<FString>(), 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
|
@ -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<FGitSourceControlState> 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<FGitSourceControlState> 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<FGitSourceControlState> 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<FGitSourceControlState> 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<FGitSourceControlState> 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<FGitSourceControlState> 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<FGitSourceControlState> 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<FGitSourceControlState> 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<FGitSourceControlState> States;
|
||||
|
||||
/** Map of filenames to history */
|
||||
TMap<FString, TGitSourceControlHistory> 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<FGitSourceControlState> 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<FGitSourceControlState> States;
|
||||
};
|
@ -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"
|
@ -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<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("GitSourceControl"));
|
||||
if(Plugin.IsValid())
|
||||
{
|
||||
UE_LOG(LogSourceControl, Log, TEXT("Git plugin '%s'"), *(Plugin->GetDescriptor().VersionName));
|
||||
}
|
||||
|
||||
CheckGitAvailability();
|
||||
|
||||
const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("GitSourceControl");
|
||||
bUsingGitLfsLocking = GitSourceControl.AccessSettings().IsUsingGitLfsLocking();
|
||||
}
|
||||
|
||||
// bForceConnection: not used anymore
|
||||
}
|
||||
|
||||
void FGitSourceControlProvider::CheckGitAvailability()
|
||||
{
|
||||
FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("GitSourceControl");
|
||||
FString PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
|
||||
if(PathToGitBinary.IsEmpty())
|
||||
{
|
||||
// Try to find Git binary, and update settings accordingly
|
||||
PathToGitBinary = GitSourceControlUtils::FindGitBinaryPath();
|
||||
if(!PathToGitBinary.IsEmpty())
|
||||
{
|
||||
GitSourceControl.AccessSettings().SetBinaryPath(PathToGitBinary);
|
||||
}
|
||||
}
|
||||
|
||||
if(!PathToGitBinary.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogSourceControl, Log, TEXT("Using '%s'"), *PathToGitBinary);
|
||||
bGitAvailable = GitSourceControlUtils::CheckGitAvailability(PathToGitBinary, &GitVersion);
|
||||
if(bGitAvailable)
|
||||
{
|
||||
CheckRepositoryStatus(PathToGitBinary);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bGitAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
void FGitSourceControlProvider::CheckRepositoryStatus(const FString& InPathToGitBinary)
|
||||
{
|
||||
// Find the path to the root Git directory (if any, else uses the ProjectDir)
|
||||
const FString PathToProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
|
||||
bGitRepositoryFound = GitSourceControlUtils::FindRootDirectory(PathToProjectDir, PathToRepositoryRoot);
|
||||
if(bGitRepositoryFound)
|
||||
{
|
||||
GitSourceControlMenu.Register();
|
||||
|
||||
// Get branch name
|
||||
bGitRepositoryFound = GitSourceControlUtils::GetBranchName(InPathToGitBinary, PathToRepositoryRoot, BranchName);
|
||||
if(bGitRepositoryFound)
|
||||
{
|
||||
GitSourceControlUtils::GetRemoteUrl(InPathToGitBinary, PathToRepositoryRoot, RemoteUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogSourceControl, Error, TEXT("'%s' is not a valid Git repository"), *PathToRepositoryRoot);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogSourceControl, Warning, TEXT("'%s' is not part of a Git repository"), *FPaths::ProjectDir());
|
||||
}
|
||||
|
||||
// Get user name & email (of the repository, else from the global Git config)
|
||||
GitSourceControlUtils::GetUserConfig(InPathToGitBinary, PathToRepositoryRoot, UserName, UserEmail);
|
||||
}
|
||||
|
||||
void FGitSourceControlProvider::Close()
|
||||
{
|
||||
// clear the cache
|
||||
StateCache.Empty();
|
||||
// Remove all extensions to the "Source Control" menu in the Editor Toolbar
|
||||
GitSourceControlMenu.Unregister();
|
||||
|
||||
bGitAvailable = false;
|
||||
bGitRepositoryFound = false;
|
||||
UserName.Empty();
|
||||
UserEmail.Empty();
|
||||
}
|
||||
|
||||
TSharedRef<FGitSourceControlState, ESPMode::ThreadSafe> FGitSourceControlProvider::GetStateInternal(const FString& Filename)
|
||||
{
|
||||
TSharedRef<FGitSourceControlState, ESPMode::ThreadSafe>* State = StateCache.Find(Filename);
|
||||
if(State != NULL)
|
||||
{
|
||||
// found cached item
|
||||
return (*State);
|
||||
}
|
||||
else
|
||||
{
|
||||
// cache an unknown state for this item
|
||||
TSharedRef<FGitSourceControlState, ESPMode::ThreadSafe> NewState = MakeShareable( new FGitSourceControlState(Filename, bUsingGitLfsLocking) );
|
||||
StateCache.Add(Filename, NewState);
|
||||
return NewState;
|
||||
}
|
||||
}
|
||||
|
||||
FText FGitSourceControlProvider::GetStatusText() const
|
||||
{
|
||||
FFormatNamedArguments Args;
|
||||
Args.Add( TEXT("RepositoryName"), FText::FromString(PathToRepositoryRoot) );
|
||||
Args.Add( TEXT("RemoteUrl"), FText::FromString(RemoteUrl) );
|
||||
Args.Add( TEXT("UserName"), FText::FromString(UserName) );
|
||||
Args.Add( TEXT("UserEmail"), FText::FromString(UserEmail) );
|
||||
Args.Add( TEXT("BranchName"), FText::FromString(BranchName) );
|
||||
Args.Add( TEXT("CommitId"), FText::FromString(CommitId.Left(8)) );
|
||||
Args.Add( TEXT("CommitSummary"), FText::FromString(CommitSummary) );
|
||||
|
||||
return FText::Format( NSLOCTEXT("Status", "Provider: Git\nEnabledLabel", "Local repository: {RepositoryName}\nRemote origin: {RemoteUrl}\nUser: {UserName}\nE-mail: {UserEmail}\n[{BranchName} {CommitId}] {CommitSummary}"), Args );
|
||||
}
|
||||
|
||||
/** Quick check if source control is enabled */
|
||||
bool FGitSourceControlProvider::IsEnabled() const
|
||||
{
|
||||
return bGitRepositoryFound;
|
||||
}
|
||||
|
||||
/** Quick check if source control is available for use (useful for server-based providers) */
|
||||
bool FGitSourceControlProvider::IsAvailable() const
|
||||
{
|
||||
return bGitRepositoryFound;
|
||||
}
|
||||
|
||||
const FName& FGitSourceControlProvider::GetName(void) const
|
||||
{
|
||||
return ProviderName;
|
||||
}
|
||||
|
||||
ECommandResult::Type FGitSourceControlProvider::GetState( const TArray<FString>& InFiles, TArray< TSharedRef<ISourceControlState, ESPMode::ThreadSafe> >& OutState, EStateCacheUsage::Type InStateCacheUsage )
|
||||
{
|
||||
if(!IsEnabled())
|
||||
{
|
||||
return ECommandResult::Failed;
|
||||
}
|
||||
|
||||
TArray<FString> AbsoluteFiles = SourceControlHelpers::AbsoluteFilenames(InFiles);
|
||||
|
||||
if(InStateCacheUsage == EStateCacheUsage::ForceUpdate)
|
||||
{
|
||||
Execute(ISourceControlOperation::Create<FUpdateStatus>(), AbsoluteFiles);
|
||||
}
|
||||
|
||||
for(const auto& AbsoluteFile : AbsoluteFiles)
|
||||
{
|
||||
OutState.Add(GetStateInternal(*AbsoluteFile));
|
||||
}
|
||||
|
||||
return ECommandResult::Succeeded;
|
||||
}
|
||||
|
||||
TArray<FSourceControlStateRef> FGitSourceControlProvider::GetCachedStateByPredicate(TFunctionRef<bool(const FSourceControlStateRef&)> Predicate) const
|
||||
{
|
||||
TArray<FSourceControlStateRef> Result;
|
||||
for(const auto& CacheItem : StateCache)
|
||||
{
|
||||
FSourceControlStateRef State = CacheItem.Value;
|
||||
if(Predicate(State))
|
||||
{
|
||||
Result.Add(State);
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
bool FGitSourceControlProvider::RemoveFileFromCache(const FString& Filename)
|
||||
{
|
||||
return StateCache.Remove(Filename) > 0;
|
||||
}
|
||||
|
||||
/** Get files in cache */
|
||||
TArray<FString> FGitSourceControlProvider::GetFilesInCache()
|
||||
{
|
||||
TArray<FString> Files;
|
||||
for (const auto& State : StateCache)
|
||||
{
|
||||
Files.Add(State.Key);
|
||||
}
|
||||
return Files;
|
||||
}
|
||||
|
||||
FDelegateHandle FGitSourceControlProvider::RegisterSourceControlStateChanged_Handle( const FSourceControlStateChanged::FDelegate& SourceControlStateChanged )
|
||||
{
|
||||
return OnSourceControlStateChanged.Add( SourceControlStateChanged );
|
||||
}
|
||||
|
||||
void FGitSourceControlProvider::UnregisterSourceControlStateChanged_Handle( FDelegateHandle Handle )
|
||||
{
|
||||
OnSourceControlStateChanged.Remove( Handle );
|
||||
}
|
||||
|
||||
ECommandResult::Type FGitSourceControlProvider::Execute( const TSharedRef<ISourceControlOperation, ESPMode::ThreadSafe>& InOperation, const TArray<FString>& InFiles, EConcurrency::Type InConcurrency, const FSourceControlOperationComplete& InOperationCompleteDelegate )
|
||||
{
|
||||
if(!IsEnabled() && !(InOperation->GetName() == "Connect")) // Only Connect operation allowed while not Enabled (Repository found)
|
||||
{
|
||||
InOperationCompleteDelegate.ExecuteIfBound(InOperation, ECommandResult::Failed);
|
||||
return ECommandResult::Failed;
|
||||
}
|
||||
|
||||
TArray<FString> AbsoluteFiles = SourceControlHelpers::AbsoluteFilenames(InFiles);
|
||||
|
||||
// Query to see if we allow this operation
|
||||
TSharedPtr<IGitSourceControlWorker, ESPMode::ThreadSafe> Worker = CreateWorker(InOperation->GetName());
|
||||
if(!Worker.IsValid())
|
||||
{
|
||||
// this operation is unsupported by this source control provider
|
||||
FFormatNamedArguments Arguments;
|
||||
Arguments.Add( TEXT("OperationName"), FText::FromName(InOperation->GetName()) );
|
||||
Arguments.Add( TEXT("ProviderName"), FText::FromName(GetName()) );
|
||||
FText Message(FText::Format(LOCTEXT("UnsupportedOperation", "Operation '{OperationName}' not supported by source control provider '{ProviderName}'"), Arguments));
|
||||
FMessageLog("SourceControl").Error(Message);
|
||||
InOperation->AddErrorMessge(Message);
|
||||
|
||||
InOperationCompleteDelegate.ExecuteIfBound(InOperation, ECommandResult::Failed);
|
||||
return ECommandResult::Failed;
|
||||
}
|
||||
|
||||
FGitSourceControlCommand* Command = new FGitSourceControlCommand(InOperation, Worker.ToSharedRef());
|
||||
Command->Files = AbsoluteFiles;
|
||||
Command->OperationCompleteDelegate = InOperationCompleteDelegate;
|
||||
|
||||
// fire off operation
|
||||
if(InConcurrency == EConcurrency::Synchronous)
|
||||
{
|
||||
Command->bAutoDelete = false;
|
||||
|
||||
UE_LOG(LogSourceControl, Log, TEXT("ExecuteSynchronousCommand(%s)"), *InOperation->GetName().ToString());
|
||||
return ExecuteSynchronousCommand(*Command, InOperation->GetInProgressString());
|
||||
}
|
||||
else
|
||||
{
|
||||
Command->bAutoDelete = true;
|
||||
|
||||
UE_LOG(LogSourceControl, Log, TEXT("IssueAsynchronousCommand(%s)"), *InOperation->GetName().ToString());
|
||||
return IssueCommand(*Command);
|
||||
}
|
||||
}
|
||||
|
||||
bool FGitSourceControlProvider::CanCancelOperation( const TSharedRef<ISourceControlOperation, ESPMode::ThreadSafe>& InOperation ) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void FGitSourceControlProvider::CancelOperation( const TSharedRef<ISourceControlOperation, ESPMode::ThreadSafe>& InOperation )
|
||||
{
|
||||
}
|
||||
|
||||
bool FGitSourceControlProvider::UsesLocalReadOnlyState() const
|
||||
{
|
||||
return bUsingGitLfsLocking; // Git LFS Lock uses read-only state
|
||||
}
|
||||
|
||||
bool FGitSourceControlProvider::UsesChangelists() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FGitSourceControlProvider::UsesCheckout() const
|
||||
{
|
||||
return bUsingGitLfsLocking; // Git LFS Lock uses read-only state
|
||||
}
|
||||
|
||||
TSharedPtr<IGitSourceControlWorker, ESPMode::ThreadSafe> FGitSourceControlProvider::CreateWorker(const FName& InOperationName) const
|
||||
{
|
||||
const FGetGitSourceControlWorker* Operation = WorkersMap.Find(InOperationName);
|
||||
if(Operation != nullptr)
|
||||
{
|
||||
return Operation->Execute();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void FGitSourceControlProvider::RegisterWorker( const FName& InName, const FGetGitSourceControlWorker& InDelegate )
|
||||
{
|
||||
WorkersMap.Add( InName, InDelegate );
|
||||
}
|
||||
|
||||
void FGitSourceControlProvider::OutputCommandMessages(const FGitSourceControlCommand& InCommand) const
|
||||
{
|
||||
FMessageLog SourceControlLog("SourceControl");
|
||||
|
||||
for(int32 ErrorIndex = 0; ErrorIndex < InCommand.ErrorMessages.Num(); ++ErrorIndex)
|
||||
{
|
||||
SourceControlLog.Error(FText::FromString(InCommand.ErrorMessages[ErrorIndex]));
|
||||
}
|
||||
|
||||
for(int32 InfoIndex = 0; InfoIndex < InCommand.InfoMessages.Num(); ++InfoIndex)
|
||||
{
|
||||
SourceControlLog.Info(FText::FromString(InCommand.InfoMessages[InfoIndex]));
|
||||
}
|
||||
}
|
||||
|
||||
void FGitSourceControlProvider::UpdateRepositoryStatus(const class FGitSourceControlCommand& InCommand)
|
||||
{
|
||||
// For all operations running UpdateStatus, get Commit informations:
|
||||
if (!InCommand.CommitId.IsEmpty())
|
||||
{
|
||||
CommitId = InCommand.CommitId;
|
||||
CommitSummary = InCommand.CommitSummary;
|
||||
}
|
||||
}
|
||||
|
||||
void FGitSourceControlProvider::Tick()
|
||||
{
|
||||
bool bStatesUpdated = false;
|
||||
|
||||
for(int32 CommandIndex = 0; CommandIndex < CommandQueue.Num(); ++CommandIndex)
|
||||
{
|
||||
FGitSourceControlCommand& Command = *CommandQueue[CommandIndex];
|
||||
if(Command.bExecuteProcessed)
|
||||
{
|
||||
// Remove command from the queue
|
||||
CommandQueue.RemoveAt(CommandIndex);
|
||||
|
||||
// Update respository status on UpdateStatus operations
|
||||
UpdateRepositoryStatus(Command);
|
||||
|
||||
// let command update the states of any files
|
||||
bStatesUpdated |= Command.Worker->UpdateStates();
|
||||
|
||||
// dump any messages to output log
|
||||
OutputCommandMessages(Command);
|
||||
|
||||
// run the completion delegate callback if we have one bound
|
||||
Command.ReturnResults();
|
||||
|
||||
// commands that are left in the array during a tick need to be deleted
|
||||
if(Command.bAutoDelete)
|
||||
{
|
||||
// Only delete commands that are not running 'synchronously'
|
||||
delete &Command;
|
||||
}
|
||||
|
||||
// only do one command per tick loop, as we dont want concurrent modification
|
||||
// of the command queue (which can happen in the completion delegate)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(bStatesUpdated)
|
||||
{
|
||||
OnSourceControlStateChanged.Broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
TArray< TSharedRef<ISourceControlLabel> > FGitSourceControlProvider::GetLabels( const FString& InMatchingSpec ) const
|
||||
{
|
||||
TArray< TSharedRef<ISourceControlLabel> > Tags;
|
||||
|
||||
// NOTE list labels. Called by CrashDebugHelper() (to remote debug Engine crash)
|
||||
// and by SourceControlHelpers::AnnotateFile() (to add source file to report)
|
||||
// Reserved for internal use by Epic Games with Perforce only
|
||||
return Tags;
|
||||
}
|
||||
|
||||
#if SOURCE_CONTROL_WITH_SLATE
|
||||
TSharedRef<class SWidget> FGitSourceControlProvider::MakeSettingsWidget() const
|
||||
{
|
||||
return SNew(SGitSourceControlSettings);
|
||||
}
|
||||
#endif
|
||||
|
||||
ECommandResult::Type FGitSourceControlProvider::ExecuteSynchronousCommand(FGitSourceControlCommand& InCommand, const FText& Task)
|
||||
{
|
||||
ECommandResult::Type Result = ECommandResult::Failed;
|
||||
|
||||
// Display the progress dialog if a string was provided
|
||||
{
|
||||
FScopedSourceControlProgress Progress(Task);
|
||||
|
||||
// Issue the command asynchronously...
|
||||
IssueCommand( InCommand );
|
||||
|
||||
// ... then wait for its completion (thus making it synchronous)
|
||||
while(!InCommand.bExecuteProcessed)
|
||||
{
|
||||
// Tick the command queue and update progress.
|
||||
Tick();
|
||||
|
||||
Progress.Tick();
|
||||
|
||||
// Sleep so we don't busy-wait so much.
|
||||
FPlatformProcess::Sleep(0.01f);
|
||||
}
|
||||
|
||||
// always do one more Tick() to make sure the command queue is cleaned up.
|
||||
Tick();
|
||||
|
||||
if(InCommand.bCommandSuccessful)
|
||||
{
|
||||
Result = ECommandResult::Succeeded;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the command now (asynchronous commands are deleted in the Tick() method)
|
||||
check(!InCommand.bAutoDelete);
|
||||
|
||||
// ensure commands that are not auto deleted do not end up in the command queue
|
||||
if ( CommandQueue.Contains( &InCommand ) )
|
||||
{
|
||||
CommandQueue.Remove( &InCommand );
|
||||
}
|
||||
delete &InCommand;
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
ECommandResult::Type FGitSourceControlProvider::IssueCommand(FGitSourceControlCommand& InCommand)
|
||||
{
|
||||
if(GThreadPool != nullptr)
|
||||
{
|
||||
// Queue this to our worker thread(s) for resolving
|
||||
GThreadPool->AddQueuedWork(&InCommand);
|
||||
CommandQueue.Add(&InCommand);
|
||||
return ECommandResult::Succeeded;
|
||||
}
|
||||
else
|
||||
{
|
||||
FText Message(LOCTEXT("NoSCCThreads", "There are no threads available to process the source control command."));
|
||||
|
||||
FMessageLog("SourceControl").Error(Message);
|
||||
InCommand.bCommandSuccessful = false;
|
||||
InCommand.Operation->AddErrorMessge(Message);
|
||||
|
||||
return InCommand.ReturnResults();
|
||||
}
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
@ -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<FString>& 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<FString>& InFiles, TArray< TSharedRef<ISourceControlState, ESPMode::ThreadSafe> >& OutState, EStateCacheUsage::Type InStateCacheUsage ) override;
|
||||
virtual TArray<FSourceControlStateRef> GetCachedStateByPredicate(TFunctionRef<bool(const FSourceControlStateRef&)> 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<ISourceControlOperation, ESPMode::ThreadSafe>& InOperation, const TArray<FString>& InFiles, EConcurrency::Type InConcurrency = EConcurrency::Synchronous, const FSourceControlOperationComplete& InOperationCompleteDelegate = FSourceControlOperationComplete()) override;
|
||||
virtual bool CanCancelOperation( const TSharedRef<ISourceControlOperation, ESPMode::ThreadSafe>& InOperation ) const override;
|
||||
virtual void CancelOperation( const TSharedRef<ISourceControlOperation, ESPMode::ThreadSafe>& InOperation ) override;
|
||||
virtual bool UsesLocalReadOnlyState() const override;
|
||||
virtual bool UsesChangelists() const override;
|
||||
virtual bool UsesCheckout() const override;
|
||||
virtual void Tick() override;
|
||||
virtual TArray< TSharedRef<class ISourceControlLabel> > GetLabels( const FString& InMatchingSpec ) const override;
|
||||
#if SOURCE_CONTROL_WITH_SLATE
|
||||
virtual TSharedRef<class SWidget> 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<FGitSourceControlState, ESPMode::ThreadSafe> 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<FString> 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<class IGitSourceControlWorker, ESPMode::ThreadSafe> 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<FString, TSharedRef<class FGitSourceControlState, ESPMode::ThreadSafe> > StateCache;
|
||||
|
||||
/** The currently registered source control operations */
|
||||
TMap<FName, FGetGitSourceControlWorker> 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;
|
||||
};
|
@ -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<FGitSourceControlModule>("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<FAnnotationLine>& 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<class ISourceControlRevision, ESPMode::ThreadSafe> 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
|
@ -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<FGitSourceControlRevision, ESPMode::ThreadSafe>
|
||||
{
|
||||
public:
|
||||
FGitSourceControlRevision()
|
||||
: RevisionNumber(0)
|
||||
{
|
||||
}
|
||||
|
||||
/** ISourceControlRevision interface */
|
||||
virtual bool Get( FString& InOutFilename ) const override;
|
||||
virtual bool GetAnnotated( TArray<FAnnotationLine>& 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<class ISourceControlRevision, ESPMode::ThreadSafe> 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<FGitSourceControlRevision, ESPMode::ThreadSafe> 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<FGitSourceControlRevision, ESPMode::ThreadSafe> > TGitSourceControlHistory;
|
@ -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);
|
||||
}
|
@ -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;
|
||||
};
|
@ -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<class ISourceControlRevision, ESPMode::ThreadSafe> FGitSourceControlState::GetHistoryItem( int32 HistoryIndex ) const
|
||||
{
|
||||
check(History.IsValidIndex(HistoryIndex));
|
||||
return History[HistoryIndex];
|
||||
}
|
||||
|
||||
TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FGitSourceControlState::FindHistoryRevision( int32 RevisionNumber ) const
|
||||
{
|
||||
for(const auto& Revision : History)
|
||||
{
|
||||
if(Revision->GetRevisionNumber() == RevisionNumber)
|
||||
{
|
||||
return Revision;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FGitSourceControlState::FindHistoryRevision(const FString& InRevision) const
|
||||
{
|
||||
for(const auto& Revision : History)
|
||||
{
|
||||
if(Revision->GetRevision() == InRevision)
|
||||
{
|
||||
return Revision;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> 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
|
@ -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<FGitSourceControlState, ESPMode::ThreadSafe>
|
||||
{
|
||||
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<class ISourceControlRevision, ESPMode::ThreadSafe> GetHistoryItem(int32 HistoryIndex) const override;
|
||||
virtual TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FindHistoryRevision(int32 RevisionNumber) const override;
|
||||
virtual TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FindHistoryRevision(const FString& InRevision) const override;
|
||||
virtual TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> 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<FString> GetCheckedOutBranches() const /* UE4.20 override */ { return TArray<FString>(); }
|
||||
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;
|
||||
};
|
@ -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<FString>& InParameters, const TArray<FString>& InFiles, TArray<FString>& OutResults, TArray<FString>& 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<FString>& InParameters, const TArray<FString>& InFiles, TArray<FString>& OutResults, TArray<FString>& 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<FString>& InFiles, TArray<FString>& OutErrorMessages, TArray<FGitSourceControlState>& 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<FString>& 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<FString> RelativeFilenames(const TArray<FString>& 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<FString> AbsoluteFilenames(const TArray<FString>& InFileNames, const FString& InRelativeTo);
|
||||
|
||||
/**
|
||||
* Helper function for various commands to update cached states.
|
||||
* @returns true if any states were updated
|
||||
*/
|
||||
bool UpdateCachedStates(const TArray<FGitSourceControlState>& 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<FString>& OutErrorMessages, TMap<FString, FString>& OutLocks);
|
||||
|
||||
}
|
@ -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<IGitSourceControlWorker, ESPMode::ThreadSafe> FGitSourceControlWorkerRef;
|
@ -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<FGitSourceControlModule>("GitSourceControl");
|
||||
return GitSourceControl.AccessSettings().GetBinaryPath();
|
||||
}
|
||||
|
||||
void SGitSourceControlSettings::OnBinaryPathPicked( const FString& PickedPath ) const
|
||||
{
|
||||
FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("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<FGitSourceControlModule>("GitSourceControl");
|
||||
return FText::FromString(GitSourceControl.GetProvider().GetPathToRepositoryRoot());
|
||||
}
|
||||
|
||||
FText SGitSourceControlSettings::GetUserName() const
|
||||
{
|
||||
const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("GitSourceControl");
|
||||
return FText::FromString(GitSourceControl.GetProvider().GetUserName());
|
||||
}
|
||||
|
||||
FText SGitSourceControlSettings::GetUserEmail() const
|
||||
{
|
||||
const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("GitSourceControl");
|
||||
return FText::FromString(GitSourceControl.GetProvider().GetUserEmail());
|
||||
}
|
||||
|
||||
EVisibility SGitSourceControlSettings::MustInitializeGitRepository() const
|
||||
{
|
||||
const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("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<FGitSourceControlModule>("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<FGitSourceControlModule>("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<FGitSourceControlModule>("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<FGitSourceControlModule>("GitSourceControl");
|
||||
const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
|
||||
const FString PathToProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
|
||||
TArray<FString> InfoMessages;
|
||||
TArray<FString> 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<FString>(), TArray<FString>(), 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<FString> Parameters;
|
||||
Parameters.Add(TEXT("add origin"));
|
||||
Parameters.Add(RemoteUrl.ToString());
|
||||
GitSourceControlUtils::RunCommand(TEXT("remote"), PathToGitBinary, PathToProjectDir, Parameters, TArray<FString>(), 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<FString> 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<FString>(), TArray<FString>(), 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<FString>& InFiles)
|
||||
{
|
||||
FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("GitSourceControl");
|
||||
TSharedRef<FMarkForAdd, ESPMode::ThreadSafe> MarkForAddOperation = ISourceControlOperation::Create<FMarkForAdd>();
|
||||
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<FCheckIn, ESPMode::ThreadSafe> CheckInOperation = ISourceControlOperation::Create<FCheckIn>();
|
||||
CheckInOperation->SetDescription(InitialCommitMessage);
|
||||
FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("GitSourceControl");
|
||||
ECommandResult::Type Result = GitSourceControl.GetProvider().Execute(CheckInOperation, TArray<FString>(), 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<FGitSourceControlModule>("GitSourceControl");
|
||||
GitSourceControl.AccessSettings().SetUsingGitLfsLocking(NewCheckedState == ECheckBoxState::Checked);
|
||||
GitSourceControl.AccessSettings().SaveSettings();
|
||||
}
|
||||
|
||||
bool SGitSourceControlSettings::GetIsUsingGitLfsLocking() const
|
||||
{
|
||||
const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("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<FGitSourceControlModule>("GitSourceControl");
|
||||
GitSourceControl.AccessSettings().SetLfsUserName(InText.ToString());
|
||||
GitSourceControl.AccessSettings().SaveSettings();
|
||||
}
|
||||
|
||||
FText SGitSourceControlSettings::GetLfsUserName() const
|
||||
{
|
||||
const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("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
|
@ -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<FString>& 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<SNotificationItem> OperationInProgressNotification;
|
||||
|
||||
void DisplayInProgressNotification(const FSourceControlOperationRef& InOperation);
|
||||
void RemoveInProgressNotification();
|
||||
void DisplaySuccessNotification(const FSourceControlOperationRef& InOperation);
|
||||
void DisplayFailureNotification(const FSourceControlOperationRef& InOperation);
|
||||
};
|
2
Plugins/UE4GitPlugin-2.17-beta/_config.yml
Normal file
@ -0,0 +1,2 @@
|
||||
show_downloads: true
|
||||
theme: jekyll-theme-slate
|