ReAdded Git

This commit is contained in:
Jesse James Isler 2021-11-17 12:35:23 +01:00
parent 6c298b7121
commit 17a23603c4
40 changed files with 6259 additions and 0 deletions

View 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+

View File

@ -0,0 +1,5 @@
/Binaries/*/*.pdb
/Binaries/*/*Debug*
/Binaries/*/*.dylib
/Binaries/*/*.modules
/Intermediate

View 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"
}
]
}

View 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.

View 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -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",
}
);
}
}

View File

@ -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;
}

View File

@ -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;
};

View File

@ -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

View File

@ -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);
};

View File

@ -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

View File

@ -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;
};

View File

@ -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

View File

@ -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;
};

View File

@ -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"

View File

@ -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

View File

@ -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;
};

View File

@ -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

View File

@ -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;

View File

@ -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);
}

View File

@ -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;
};

View File

@ -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

View File

@ -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;
};

View File

@ -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);
}

View File

@ -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;

View File

@ -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

View File

@ -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);
};

View File

@ -0,0 +1,2 @@
show_downloads: true
theme: jekyll-theme-slate