Added git plugin
This commit is contained in:
parent
dd5999f2ff
commit
4c04805c5a
12
Plugins/UEGitPlugin/.gitignore
vendored
Normal file
12
Plugins/UEGitPlugin/.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# PBGet
|
||||||
|
Binaries/
|
||||||
|
/Intermediate
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
### whitelisted files
|
||||||
|
!Resources/*
|
||||||
|
!Screenshots/*
|
||||||
|
!git-lfs.exe
|
||||||
|
!git-lfs
|
||||||
|
!git-lfs-mac-amd64
|
||||||
|
!git-lfs-mac-arm64
|
23
Plugins/UEGitPlugin/GitSourceControl.uplugin
Normal file
23
Plugins/UEGitPlugin/GitSourceControl.uplugin
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"FileVersion": 3,
|
||||||
|
"Version": 316,
|
||||||
|
"VersionName": "3.16",
|
||||||
|
"FriendlyName": "Git LFS 2",
|
||||||
|
"Description": "Git source control management",
|
||||||
|
"Category": "Source Control",
|
||||||
|
"CreatedBy": "Project Borealis",
|
||||||
|
"CreatedByURL": "https://projectborealis.com",
|
||||||
|
"DocsURL": "https://github.com/ProjectBorealis/UEGitPlugin/blob/release/README.md",
|
||||||
|
"SupportURL": "https://github.com/ProjectBorealis/UEGitPlugin/issues",
|
||||||
|
"EnabledByDefault": true,
|
||||||
|
"CanContainContent": false,
|
||||||
|
"IsBetaVersion": false,
|
||||||
|
"Installed": false,
|
||||||
|
"Modules": [
|
||||||
|
{
|
||||||
|
"Name": "GitSourceControl",
|
||||||
|
"Type": "UncookedOnly",
|
||||||
|
"LoadingPhase": "Default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
21
Plugins/UEGitPlugin/LICENSE.txt
Normal file
21
Plugins/UEGitPlugin/LICENSE.txt
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2021-2023 Project Borealis
|
||||||
|
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.
|
214
Plugins/UEGitPlugin/README.md
Normal file
214
Plugins/UEGitPlugin/README.md
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
# Unreal Engine Git Plugin
|
||||||
|
|
||||||
|
This is a refactor of the [Git LFS 2 plugin by SRombauts](https://github.com/SRombauts/UE4GitPlugin), with lessons learned from production
|
||||||
|
that include performance optimizations, new features and workflow improvements.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* Multi-threaded locking/unlocking, greatly improving performance when locking/unlocking many files
|
||||||
|
* Remote lock status only checked when needed
|
||||||
|
* Added local lock cache for speeding up local operations
|
||||||
|
* Improved performance of repository file traversal
|
||||||
|
* Improved initialization logic
|
||||||
|
* Generally improved wording and UX throughout
|
||||||
|
* Greatly improved pull within editor
|
||||||
|
* Only refreshes changed files, which prevents crashes in large projects
|
||||||
|
* Uses rebase workflow to properly manage local work
|
||||||
|
* Added support for Status Branches, which check outdated files vs. remote across multiple branches
|
||||||
|
* Periodic background remote refresh to keep remote file statuses up to date
|
||||||
|
* Automatic handling of pushing from an outdated local copy
|
||||||
|
* Optimized status updates for successful operations
|
||||||
|
* Manage both lockable (assets, maps) and non-lockable files (configs, project file) in editor
|
||||||
|
* Improved status display in editor
|
||||||
|
* Integration with [PBSync](https://github.com/ProjectBorealis/PBSync) binaries syncing
|
||||||
|
* General improvements to performance and memory usage
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Either install this into your project's `Plugins/` folder, or if you would like to install to the engine,
|
||||||
|
rename `Engine/Plugins/Developer/GitSourceControl.uplugin` to `Engine/Plugins/Developer/GitSourceControl.uplugin.disabled`
|
||||||
|
and then install this plugin to the `Engine/Plugins` folder.
|
||||||
|
|
||||||
|
### Note about .gitattributes and .gitignore
|
||||||
|
|
||||||
|
This plugin requires explicit file attributes for `*.umap` and `*.uasset`, rather than other approaches of using wildcards for the content folder (`Content/**`).
|
||||||
|
|
||||||
|
See [our own `.gitattributes`](https://github.com/ProjectBorealis/PBCore/blob/main/.gitattributes) for an example.
|
||||||
|
|
||||||
|
You may also want to check out [our robust `.gitignore`](https://github.com/ProjectBorealis/PBCore/blob/main/.gitignore) too.
|
||||||
|
|
||||||
|
### Note about authentication
|
||||||
|
|
||||||
|
We would highly recommend using HTTPS authentication for your Git repo.
|
||||||
|
|
||||||
|
This allows a single credential path to be used, with the robust and fast HTTPS support in LFS.
|
||||||
|
|
||||||
|
With [Git Credential Manager](https://github.com/GitCredentialManager/git-credential-manager), authenticating with HTTPS is also much easier, with a GUI available to authenticate with any Git provider.
|
||||||
|
|
||||||
|
### Note about Unreal configuration
|
||||||
|
|
||||||
|
#### Required
|
||||||
|
|
||||||
|
* The plugin makes the assumption that files are always explicitly added. We made this decision because it is beneficial for performance and our workflows. In `Config/DefaultEditorPerProjectUserSettings.ini`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[/Script/UnrealEd.EditorLoadingSavingSettings]
|
||||||
|
bSCCAutoAddNewFiles=False
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Recommended
|
||||||
|
|
||||||
|
* As a general revision control usability improvement, you can enable new checkout features in `Config/DefaultEditorPerProjectUserSettings.ini`. To enable auto-checkout on modification, which is great for OFPA and other workflows (but requires user attention to excessive locking of content):
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[/Script/UnrealEd.EditorLoadingSavingSettings]
|
||||||
|
bAutomaticallyCheckoutOnAssetModification=True
|
||||||
|
bPromptForCheckoutOnAssetModification=False
|
||||||
|
```
|
||||||
|
|
||||||
|
* OR, to enable auto-prompt on modification, which is a bit more upfront/intrusive in user flows, but more conservative with locking, flip the settings:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[/Script/UnrealEd.EditorLoadingSavingSettings]
|
||||||
|
bAutomaticallyCheckoutOnAssetModification=False
|
||||||
|
bPromptForCheckoutOnAssetModification=True
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
* As another general usability improvement, you can set the editor to load any checked out packages for faster loading. In `Config/DefaultEditorPerProjectUserSettings.ini`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[/Script/UnrealEd.EditorPerProjectUserSettings]
|
||||||
|
bAutoloadCheckedOutPackages=True
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
* In `Config/DefaultEngine.ini` you can set this option to `1` to disable a feature that is unnecessary for Git (for performance):
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[SystemSettingsEditor]
|
||||||
|
r.Editor.SkipSourceControlCheckForEditablePackages=1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Status Branches - Required Code Changes
|
||||||
|
|
||||||
|
Epic Games added Status Branches in 4.20, and this plugin has implemented support for them. See [Workflow on Fortnite](https://youtu.be/p4RcDpGQ_tI?t=1443) for more information. Here is an example of how you may apply it to your own game.
|
||||||
|
|
||||||
|
1. Make an `UUnrealEdEngine` subclass, preferrably in an editor only module, or guarded by `WITH_EDITOR`.
|
||||||
|
2. Add the following:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include "ISourceControlModule.h"
|
||||||
|
#include "ISourceControlProvider.h"
|
||||||
|
|
||||||
|
void UMyEdEngine::Init(IEngineLoop* InEngineLoop)
|
||||||
|
{
|
||||||
|
Super::Init(InEngineLoop);
|
||||||
|
|
||||||
|
// Register state branches
|
||||||
|
const ISourceControlModule& SourceControlModule = ISourceControlModule::Get();
|
||||||
|
{
|
||||||
|
ISourceControlProvider& SourceControlProvider = SourceControlModule.GetProvider();
|
||||||
|
// Order matters. Lower values are lower in the hierarchy, i.e., changes from higher branches get automatically merged down.
|
||||||
|
// (Automatic merging requires an appropriately configured CI pipeline)
|
||||||
|
// With this paradigm, the higher the branch is, the stabler it is, and has changes manually promoted up.
|
||||||
|
const TArray<FString> Branches {"origin/develop", "origin/promoted"};
|
||||||
|
SourceControlProvider.RegisterStateBranches(Branches, TEXT("Content"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Set to use the editor engine in `Config/DefaultEngine.ini` (make sure the class name is `MyUnrealEdEngine` for a class called `UMyUnrealEdEngine`!):
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[/Script/Engine.Engine]
|
||||||
|
UnrealEdEngine=/Script/MyModule.MyEdEngine
|
||||||
|
```
|
||||||
|
|
||||||
|
5. In this example, `origin/promoted` is the highest tested branch. Any changes in this branch are asset changes that do not need testing, and get automatically merged down to `origin/develop`. This may be extended to involve multiple branches, like `origin/trunk`, `origin/main`, or whatever you may prefer, where changes may be cascaded from most-stable to least-stable automatically. With this paradigm, changes from less-stable branches are manually promoted to more-stable branches after a merge review.
|
||||||
|
**NOTE**: The second argument in `RegisterStateBranches` is Perforce specific and is ignored, but is meant to point to the relative content path.
|
||||||
|
|
||||||
|
6. If you decide to implement the status branch code in a editor-only module, ensure the loading phase in the editor module is set to `Default` in your .uproject settings, like so: (Otherwise, the editor will likely have difficulty finding your subclass'd UUnrealEdEngine class.)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Name": "MyTestProjectEditor",
|
||||||
|
"Type": "Editor",
|
||||||
|
"LoadingPhase": "Default"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Status Branches - Conceptual Overview
|
||||||
|
|
||||||
|
This feature helps ensure you're not locking and modifying files that are out-of-date.
|
||||||
|
|
||||||
|
If a user is on **any** branch, regardless if it's tracking a branch included in the 'status branch' list, they will be **unable to checkout** files that have **more recent changes on the remote server** than they have on the local branch, **provided** those changes are in a branch in the **'status branch' list.**
|
||||||
|
* **If** the **remote branch with the changes** is **not** in the status branch list, the user will **not be notified of remote changes.**
|
||||||
|
* **If** the user makes changes to a **local branch** and **switches** to **another local branch**, the user will **not** be notified of their **own changes** to the other branch, **regardless** if it's in the 'status branch' list or not **(this feature only checks remote branches!)**
|
||||||
|
* **If** the user is tracking a remote branch that is in the status branch list, they will be **unable to lock stale files** (files that are changed up-stream).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Note:
|
||||||
|
|
||||||
|
It's important to only release file locks after changes have been pushed to the server. The system has no way to determine that there are local changes to a file, so if you modify a locked file it's imperative that you push the changes to a remote branch included in the 'status branch' list so other users can see those changes and avoid modifying a stale file. Otherwise, you'll want to keep the file locked!
|
||||||
|
|
||||||
|
Additionally, if you're switching back and forth between two or more branches locally you'll need to keep track of what branch you've made changes to locked files, as the system will not prevent you from modifying the same locked file on multiple different branches!
|
||||||
|
|
||||||
|
#### Real-world example of the 'status branch' feature:
|
||||||
|
|
||||||
|
* The user has checked out the `develop` branch, but there is an up-stream change on `origin/develop` for `FirstPersonProjectileMaterial`, indicated with the **yellow** exclamation mark.
|
||||||
|
* There are also newer upstream changes on the `promoted` branch, indicated with the **red** exclamation mark. (NOTE: The plugin does not currently report the branch name the changes are on.)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## General In-Editor Usage
|
||||||
|
|
||||||
|
### Connecting to revision control:
|
||||||
|
|
||||||
|
Generally speaking, the field next to `Uses Git LFS 2 File Locking workflow` should match your Git server's `User Name`, like so:
|
||||||
|
(If you find that the checkmark turns blue shortly after checking out a file, then the LFS name is incorrect, update it to the name it says checked out the file)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Checking out (locking) one or more assets:
|
||||||
|
|
||||||
|
You can lock individual files or you can hold `shift` to select and lock multiple at once, which can be quite a bit faster than locking them individually.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Unlocking one or more un-changed assets:
|
||||||
|
|
||||||
|
You can unlock individual files or you can hold `shift` to select and unlock multiple at once, which can be quite a bit faster than unlocking them individually.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Locking every asset within a folder:
|
||||||
|
|
||||||
|
You can lock every file in a folder by right clicking on the folder and clicking `Check Out`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Viewing locks:
|
||||||
|
|
||||||
|
View the owner of a file lock simply by hovering over the asset icon. Your locked files have a **red** check-mark, other user's locks will show up with a **blue** checkmark.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Pulling latest from within the editor:
|
||||||
|
|
||||||
|
You can pull the latest changes from your currently checked-out branch within the editor. This doesn't always work smoothly, but effort has been made to improve this process. It is still recommended to always save changes before doing this, however.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Submitting changes up-stream:
|
||||||
|
|
||||||
|
`Submit to revision control` will create a local commit, push it, and release your file lock.
|
||||||
|
(While you cannot check out branches within the plugin, it is fully branch-aware! In this scenario, the user has checked out the `develop` branch, so their change is pushed to `origin/develop`.)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
You can learn more about how we set up our Git repository at [the PBCore wiki](https://github.com/ProjectBorealis/PBCore/wiki).
|
BIN
Plugins/UEGitPlugin/Resources/Icon128.png
(Stored with Git LFS)
Normal file
BIN
Plugins/UEGitPlugin/Resources/Icon128.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
Plugins/UEGitPlugin/Screenshots/FileHistory.png
(Stored with Git LFS)
Normal file
BIN
Plugins/UEGitPlugin/Screenshots/FileHistory.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
Plugins/UEGitPlugin/Screenshots/Icons/Added.png
(Stored with Git LFS)
Normal file
BIN
Plugins/UEGitPlugin/Screenshots/Icons/Added.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
Plugins/UEGitPlugin/Screenshots/Icons/Modified.png
(Stored with Git LFS)
Normal file
BIN
Plugins/UEGitPlugin/Screenshots/Icons/Modified.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
Plugins/UEGitPlugin/Screenshots/Icons/New.png
(Stored with Git LFS)
Normal file
BIN
Plugins/UEGitPlugin/Screenshots/Icons/New.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
Plugins/UEGitPlugin/Screenshots/Icons/Renamed.png
(Stored with Git LFS)
Normal file
BIN
Plugins/UEGitPlugin/Screenshots/Icons/Renamed.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
Plugins/UEGitPlugin/Screenshots/Icons/Unchanged.png
(Stored with Git LFS)
Normal file
BIN
Plugins/UEGitPlugin/Screenshots/Icons/Unchanged.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
Plugins/UEGitPlugin/Screenshots/SourceControlLogin_Init.png
(Stored with Git LFS)
Normal file
BIN
Plugins/UEGitPlugin/Screenshots/SourceControlLogin_Init.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
Plugins/UEGitPlugin/Screenshots/SourceControlMenu.png
(Stored with Git LFS)
Normal file
BIN
Plugins/UEGitPlugin/Screenshots/SourceControlMenu.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
Plugins/UEGitPlugin/Screenshots/SourceControlStatusTooltip.png
(Stored with Git LFS)
Normal file
BIN
Plugins/UEGitPlugin/Screenshots/SourceControlStatusTooltip.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
Plugins/UEGitPlugin/Screenshots/SubmitFiles.png
(Stored with Git LFS)
Normal file
BIN
Plugins/UEGitPlugin/Screenshots/SubmitFiles.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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)
|
||||||
|
{
|
||||||
|
PrivateDependencyModuleNames.AddRange(
|
||||||
|
new string[] {
|
||||||
|
"Core",
|
||||||
|
"CoreUObject",
|
||||||
|
"Slate",
|
||||||
|
"SlateCore",
|
||||||
|
"InputCore",
|
||||||
|
"DesktopWidgets",
|
||||||
|
"EditorStyle",
|
||||||
|
"UnrealEd",
|
||||||
|
"SourceControl",
|
||||||
|
"SourceControlWindows",
|
||||||
|
"Projects"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Target.Version.MajorVersion == 5)
|
||||||
|
{
|
||||||
|
PrivateDependencyModuleNames.Add("ToolMenus");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ISourceControlModule.h"
|
||||||
|
#include "Internationalization/Text.h"
|
||||||
|
#include "Logging/MessageLog.h"
|
||||||
|
#include "Logging/TokenizedMessage.h"
|
||||||
|
#include "Templates/SharedPointer.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A thread safe replacement for FMessageLog which can be called from background threads.
|
||||||
|
* It only exposes methods from FMessageLog that we would be able to safely delay, such
|
||||||
|
* as messages. We do not provide any functionality to open error dialogs etc.
|
||||||
|
* At the moment if we detect a message is being queued when not on the game thread we log
|
||||||
|
* it instead of sending to the FMessageLog system. In the future we will store the messages
|
||||||
|
* and marshal them to the GameThread so that they can be displayed as originally intended.
|
||||||
|
*/
|
||||||
|
class FTSMessageLog
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FTSMessageLog() = delete;
|
||||||
|
FTSMessageLog(const FName& InLogName)
|
||||||
|
: Log(InLogName)
|
||||||
|
{}
|
||||||
|
|
||||||
|
FTSMessageLog(FTSMessageLog&&) = default;
|
||||||
|
FTSMessageLog& operator = (FTSMessageLog&&) = default;
|
||||||
|
|
||||||
|
FTSMessageLog(const FTSMessageLog&) = delete;
|
||||||
|
FTSMessageLog& operator = (const FTSMessageLog&) = delete;
|
||||||
|
|
||||||
|
~FTSMessageLog() = default;
|
||||||
|
|
||||||
|
TSharedRef<FTokenizedMessage> Message(EMessageSeverity::Type InSeverity, const FText& InMessage = FText())
|
||||||
|
{
|
||||||
|
if (IsInGameThread())
|
||||||
|
{
|
||||||
|
return Log.Message(InSeverity, InMessage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TSharedRef<FTokenizedMessage> Message = FTokenizedMessage::Create(InSeverity, InMessage);
|
||||||
|
UE_LOG(LogSourceControl, Display, TEXT("%s"), *Message->ToText().ToString());
|
||||||
|
|
||||||
|
return Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedRef<FTokenizedMessage> Error(const FText& InMessage = FText())
|
||||||
|
{
|
||||||
|
if (IsInGameThread())
|
||||||
|
{
|
||||||
|
return Log.Error(InMessage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TSharedRef<FTokenizedMessage> Message = FTokenizedMessage::Create(EMessageSeverity::Error, InMessage);
|
||||||
|
UE_LOG(LogSourceControl, Error, TEXT("%s"), *Message->ToText().ToString());
|
||||||
|
|
||||||
|
return Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedRef<FTokenizedMessage> PerformanceWarning(const FText& InMessage = FText())
|
||||||
|
{
|
||||||
|
if (IsInGameThread())
|
||||||
|
{
|
||||||
|
return Log.PerformanceWarning(InMessage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TSharedRef<FTokenizedMessage> Message = FTokenizedMessage::Create(EMessageSeverity::PerformanceWarning, InMessage);
|
||||||
|
UE_LOG(LogSourceControl, Warning, TEXT("%s"), *Message->ToText().ToString());
|
||||||
|
|
||||||
|
return Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedRef<FTokenizedMessage> Warning(const FText& InMessage = FText())
|
||||||
|
{
|
||||||
|
if (IsInGameThread())
|
||||||
|
{
|
||||||
|
return Log.Warning(InMessage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TSharedRef<FTokenizedMessage> Message = FTokenizedMessage::Create(EMessageSeverity::Warning, InMessage);
|
||||||
|
UE_LOG(LogSourceControl, Warning, TEXT("%s"), *Message->ToText().ToString());
|
||||||
|
|
||||||
|
return Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedRef<FTokenizedMessage> Info(const FText& InMessage = FText())
|
||||||
|
{
|
||||||
|
if (IsInGameThread())
|
||||||
|
{
|
||||||
|
return Log.Info(InMessage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TSharedRef<FTokenizedMessage> Message = FTokenizedMessage::Create(EMessageSeverity::Info, InMessage);
|
||||||
|
UE_LOG(LogSourceControl, Display, TEXT("%s"), *Message->ToText().ToString());
|
||||||
|
|
||||||
|
return Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
FMessageLog Log;
|
||||||
|
};
|
@ -0,0 +1,4 @@
|
|||||||
|
#include "GitSourceControlChangelist.h"
|
||||||
|
|
||||||
|
FGitSourceControlChangelist FGitSourceControlChangelist::WorkingChangelist(TEXT("Working"), true);
|
||||||
|
FGitSourceControlChangelist FGitSourceControlChangelist::StagedChangelist(TEXT("Staged"), true);
|
@ -0,0 +1,79 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "ISourceControlChangelist.h"
|
||||||
|
|
||||||
|
class FGitSourceControlChangelist : public ISourceControlChangelist
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FGitSourceControlChangelist() = default;
|
||||||
|
|
||||||
|
explicit FGitSourceControlChangelist(FString&& InChangelistName, const bool bInInitialized = false)
|
||||||
|
: ChangelistName(MoveTemp(InChangelistName))
|
||||||
|
, bInitialized(bInInitialized)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool CanDelete() const override
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator==(const FGitSourceControlChangelist& InOther) const
|
||||||
|
{
|
||||||
|
return ChangelistName == InOther.ChangelistName;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator!=(const FGitSourceControlChangelist& InOther) const
|
||||||
|
{
|
||||||
|
return ChangelistName != InOther.ChangelistName;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
|
||||||
|
virtual bool IsDefault() const override
|
||||||
|
{
|
||||||
|
return ChangelistName == WorkingChangelist.ChangelistName;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void SetInitialized()
|
||||||
|
{
|
||||||
|
bInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsInitialized() const
|
||||||
|
{
|
||||||
|
return bInitialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Reset()
|
||||||
|
{
|
||||||
|
ChangelistName.Reset();
|
||||||
|
bInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
friend FORCEINLINE uint32 GetTypeHash(const FGitSourceControlChangelist& InGitChangelist)
|
||||||
|
{
|
||||||
|
return GetTypeHash(InGitChangelist.ChangelistName);
|
||||||
|
}
|
||||||
|
|
||||||
|
FString GetName() const
|
||||||
|
{
|
||||||
|
return ChangelistName;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
|
||||||
|
virtual FString GetIdentifier() const override
|
||||||
|
{
|
||||||
|
return ChangelistName;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public:
|
||||||
|
static FGitSourceControlChangelist WorkingChangelist;
|
||||||
|
static FGitSourceControlChangelist StagedChangelist;
|
||||||
|
|
||||||
|
private:
|
||||||
|
FString ChangelistName;
|
||||||
|
bool bInitialized = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef TSharedRef<class FGitSourceControlChangelist, ESPMode::ThreadSafe> FGitSourceControlChangelistRef;
|
@ -0,0 +1,74 @@
|
|||||||
|
#include "GitSourceControlChangelistState.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "GitSourceControl.ChangelistState"
|
||||||
|
|
||||||
|
FName FGitSourceControlChangelistState::GetIconName() const
|
||||||
|
{
|
||||||
|
// Mimic P4V colors, returning the red icon if there are active file(s), the blue if the changelist is empty or all the files are shelved.
|
||||||
|
return FName("SourceControl.Changelist");
|
||||||
|
}
|
||||||
|
|
||||||
|
FName FGitSourceControlChangelistState::GetSmallIconName() const
|
||||||
|
{
|
||||||
|
return GetIconName();
|
||||||
|
}
|
||||||
|
|
||||||
|
FText FGitSourceControlChangelistState::GetDisplayText() const
|
||||||
|
{
|
||||||
|
return FText::FromString(Changelist.GetName());
|
||||||
|
}
|
||||||
|
|
||||||
|
FText FGitSourceControlChangelistState::GetDescriptionText() const
|
||||||
|
{
|
||||||
|
return FText::FromString(Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
FText FGitSourceControlChangelistState::GetDisplayTooltip() const
|
||||||
|
{
|
||||||
|
return LOCTEXT("Tooltip", "Tooltip");
|
||||||
|
}
|
||||||
|
|
||||||
|
const FDateTime& FGitSourceControlChangelistState::GetTimeStamp() const
|
||||||
|
{
|
||||||
|
return TimeStamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4
|
||||||
|
const TArray<FSourceControlStateRef> FGitSourceControlChangelistState::GetFilesStates() const
|
||||||
|
#else
|
||||||
|
const TArray<FSourceControlStateRef>& FGitSourceControlChangelistState::GetFilesStates() const
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
return Files;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4
|
||||||
|
int32 FGitSourceControlChangelistState::GetFilesStatesNum() const
|
||||||
|
{
|
||||||
|
return Files.Num();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4
|
||||||
|
const TArray<FSourceControlStateRef> FGitSourceControlChangelistState::GetShelvedFilesStates() const
|
||||||
|
#else
|
||||||
|
const TArray<FSourceControlStateRef>& FGitSourceControlChangelistState::GetShelvedFilesStates() const
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
return ShelvedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4
|
||||||
|
int32 FGitSourceControlChangelistState::GetShelvedFilesStatesNum() const
|
||||||
|
{
|
||||||
|
return ShelvedFiles.Num();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
FSourceControlChangelistRef FGitSourceControlChangelistState::GetChangelist() const
|
||||||
|
{
|
||||||
|
FGitSourceControlChangelistRef ChangelistCopy = MakeShareable( new FGitSourceControlChangelist(Changelist));
|
||||||
|
return StaticCastSharedRef<ISourceControlChangelist>(ChangelistCopy);
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
@ -0,0 +1,84 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "GitSourceControlChangelist.h"
|
||||||
|
#include "ISourceControlChangelistState.h"
|
||||||
|
#include "ISourceControlState.h"
|
||||||
|
|
||||||
|
class FGitSourceControlChangelistState : public ISourceControlChangelistState
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit FGitSourceControlChangelistState(const FGitSourceControlChangelist& InChangelist,
|
||||||
|
const FString& InDescription = FString())
|
||||||
|
: Changelist(InChangelist)
|
||||||
|
, Description(InDescription)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
explicit FGitSourceControlChangelistState(FGitSourceControlChangelist&& InChangelist,
|
||||||
|
FString&& InDescription)
|
||||||
|
: Changelist(MoveTemp(InChangelist))
|
||||||
|
, Description(MoveTemp(InDescription))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of the icon graphic we should use to display the state in a UI.
|
||||||
|
* @returns the name of the icon to display
|
||||||
|
*/
|
||||||
|
virtual FName GetIconName() const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of the small icon graphic we should use to display the state in a UI.
|
||||||
|
* @returns the name of the icon to display
|
||||||
|
*/
|
||||||
|
virtual FName GetSmallIconName() const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a text representation of the state
|
||||||
|
* @returns the text to display for this state
|
||||||
|
*/
|
||||||
|
virtual FText GetDisplayText() const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a text representation of the state
|
||||||
|
* @returns the text to display for this state
|
||||||
|
*/
|
||||||
|
virtual FText GetDescriptionText() const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a tooltip to describe this state
|
||||||
|
* @returns the text to display for this states tooltip
|
||||||
|
*/
|
||||||
|
virtual FText GetDisplayTooltip() const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the timestamp of the last update that was made to this state.
|
||||||
|
* @returns the timestamp of the last update
|
||||||
|
*/
|
||||||
|
virtual const FDateTime& GetTimeStamp() const override;
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4
|
||||||
|
virtual const TArray<FSourceControlStateRef> GetFilesStates() const override;
|
||||||
|
virtual int32 GetFilesStatesNum() const override;
|
||||||
|
|
||||||
|
virtual const TArray<FSourceControlStateRef> GetShelvedFilesStates() const override;
|
||||||
|
virtual int32 GetShelvedFilesStatesNum() const override;
|
||||||
|
#else
|
||||||
|
virtual const TArray<FSourceControlStateRef>& GetFilesStates() const override;
|
||||||
|
|
||||||
|
virtual const TArray<FSourceControlStateRef>& GetShelvedFilesStates() const override;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
virtual FSourceControlChangelistRef GetChangelist() const override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
FGitSourceControlChangelist Changelist;
|
||||||
|
|
||||||
|
FString Description;
|
||||||
|
|
||||||
|
TArray<FSourceControlStateRef> Files;
|
||||||
|
|
||||||
|
TArray<FSourceControlStateRef> ShelvedFiles;
|
||||||
|
|
||||||
|
/** The timestamp of the last update */
|
||||||
|
FDateTime TimeStamp;
|
||||||
|
};
|
@ -0,0 +1,82 @@
|
|||||||
|
// Copyright (c) 2014-2023 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"
|
||||||
|
#include "GitSourceControlUtils.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)
|
||||||
|
, bCancelled(0)
|
||||||
|
, bCommandSuccessful(false)
|
||||||
|
, bAutoDelete(true)
|
||||||
|
, Concurrency(EConcurrency::Synchronous)
|
||||||
|
{
|
||||||
|
// cache the providers settings here
|
||||||
|
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
const FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
|
||||||
|
PathToGitBinary = Provider.GetGitBinaryPath();
|
||||||
|
bUsingGitLfsLocking = Provider.UsesCheckout();
|
||||||
|
PathToRepositoryRoot = Provider.GetPathToRepositoryRoot();
|
||||||
|
PathToGitRoot = Provider.GetPathToGitRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlCommand::UpdateRepositoryRootIfSubmodule(const TArray<FString>& AbsoluteFilePaths)
|
||||||
|
{
|
||||||
|
PathToRepositoryRoot = GitSourceControlUtils::ChangeRepositoryRootIfSubmodule(AbsoluteFilePaths, PathToRepositoryRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlCommand::Cancel()
|
||||||
|
{
|
||||||
|
FPlatformAtomics::InterlockedExchange(&bCancelled, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlCommand::IsCanceled() const
|
||||||
|
{
|
||||||
|
return bCancelled != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ECommandResult::Type FGitSourceControlCommand::ReturnResults()
|
||||||
|
{
|
||||||
|
// Save any messages that have accumulated
|
||||||
|
for (const auto& String : ResultInfo.InfoMessages)
|
||||||
|
{
|
||||||
|
Operation->AddInfoMessge(FText::FromString(String));
|
||||||
|
}
|
||||||
|
for (const auto& String : ResultInfo.ErrorMessages)
|
||||||
|
{
|
||||||
|
Operation->AddErrorMessge(FText::FromString(String));
|
||||||
|
}
|
||||||
|
|
||||||
|
// run the completion delegate if we have one bound
|
||||||
|
ECommandResult::Type Result = bCancelled ? ECommandResult::Cancelled : (bCommandSuccessful ? ECommandResult::Succeeded : ECommandResult::Failed);
|
||||||
|
OperationCompleteDelegate.ExecuteIfBound(Operation, Result);
|
||||||
|
|
||||||
|
return Result;
|
||||||
|
}
|
@ -0,0 +1,128 @@
|
|||||||
|
// Copyright (c) 2014-2023 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 "GitSourceControlChangelist.h"
|
||||||
|
#include "ISourceControlProvider.h"
|
||||||
|
#include "Misc/IQueuedWork.h"
|
||||||
|
|
||||||
|
/** Accumulated error and info messages for a revision control operation. */
|
||||||
|
struct FGitSourceControlResultInfo
|
||||||
|
{
|
||||||
|
/** Append any messages from another FSourceControlResultInfo, ensuring to keep any already accumulated info. */
|
||||||
|
void Append(const FGitSourceControlResultInfo& InResultInfo)
|
||||||
|
{
|
||||||
|
InfoMessages.Append(InResultInfo.InfoMessages);
|
||||||
|
ErrorMessages.Append(InResultInfo.ErrorMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Info and/or warning message storage */
|
||||||
|
TArray<FString> InfoMessages;
|
||||||
|
|
||||||
|
/** Potential error message storage */
|
||||||
|
TArray<FString> ErrorMessages;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modify the repo root if all selected files are in a plugin subfolder, and the plugin subfolder is a git repo
|
||||||
|
* This supports the case where each plugin is a sub module
|
||||||
|
*/
|
||||||
|
void UpdateRepositoryRootIfSubmodule(const TArray<FString>& AbsoluteFilePaths);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/** Attempt to cancel the operation */
|
||||||
|
void Cancel();
|
||||||
|
|
||||||
|
/** Is the operation canceled? */
|
||||||
|
bool IsCanceled() const;
|
||||||
|
|
||||||
|
/** 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 Unreal revision control repository: usually the ProjectDir */
|
||||||
|
FString PathToRepositoryRoot;
|
||||||
|
|
||||||
|
/** Path to the root of the Git repository: can be the ProjectDir itself, or any parent directory (found by the "Connect" operation) */
|
||||||
|
FString PathToGitRoot;
|
||||||
|
|
||||||
|
/** 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 revision control thread*/
|
||||||
|
volatile int32 bExecuteProcessed;
|
||||||
|
|
||||||
|
/**If true, this command has been cancelled*/
|
||||||
|
volatile int32 bCancelled;
|
||||||
|
|
||||||
|
/**If true, the revision control command succeeded*/
|
||||||
|
bool bCommandSuccessful;
|
||||||
|
|
||||||
|
/** 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;
|
||||||
|
|
||||||
|
/** Changelist to perform this operation on */
|
||||||
|
FGitSourceControlChangelist Changelist;
|
||||||
|
|
||||||
|
/** Potential error, warning and info message storage */
|
||||||
|
FGitSourceControlResultInfo ResultInfo;
|
||||||
|
|
||||||
|
/** Branch names for status queries */
|
||||||
|
TArray< FString > StatusBranchNames;
|
||||||
|
};
|
@ -0,0 +1,44 @@
|
|||||||
|
// Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
|
||||||
|
|
||||||
|
#include "GitSourceControlConsole.h"
|
||||||
|
|
||||||
|
#include "HAL/IConsoleManager.h"
|
||||||
|
#include "ISourceControlModule.h"
|
||||||
|
|
||||||
|
#include "GitSourceControlModule.h"
|
||||||
|
#include "GitSourceControlUtils.h"
|
||||||
|
|
||||||
|
// Auto-registered console commands:
|
||||||
|
// No re-register on hot reload, and unregistered only once on editor shutdown.
|
||||||
|
static FAutoConsoleCommand g_executeGitConsoleCommand(TEXT("git"),
|
||||||
|
TEXT("Git Command Line Interface.\n")
|
||||||
|
TEXT("Run any 'git' command directly from the Unreal Editor Console.\n")
|
||||||
|
TEXT("Type 'git help' to get a command list."),
|
||||||
|
FConsoleCommandWithArgsDelegate::CreateStatic(&GitSourceControlConsole::ExecuteGitConsoleCommand));
|
||||||
|
|
||||||
|
void GitSourceControlConsole::ExecuteGitConsoleCommand(const TArray<FString>& a_args)
|
||||||
|
{
|
||||||
|
FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked<FGitSourceControlModule>("GitSourceControl");
|
||||||
|
const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
|
||||||
|
const FString& RepositoryRoot = GitSourceControl.GetProvider().GetPathToRepositoryRoot();
|
||||||
|
|
||||||
|
// The first argument is the command to send to git, the following ones are forwarded as parameters for the command
|
||||||
|
TArray<FString> Parameters = a_args;
|
||||||
|
FString Command;
|
||||||
|
if (a_args.Num() > 0)
|
||||||
|
{
|
||||||
|
Command = a_args[0];
|
||||||
|
Parameters.RemoveAt(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If no command is provided, use "help" to emulate the behavior of the git CLI
|
||||||
|
Command = TEXT("help");
|
||||||
|
}
|
||||||
|
|
||||||
|
FString Results;
|
||||||
|
FString Errors;
|
||||||
|
GitSourceControlUtils::RunCommandInternalRaw(Command, PathToGitBinary, RepositoryRoot, Parameters, TArray<FString>(), Results, Errors);
|
||||||
|
|
||||||
|
UE_LOG(LogSourceControl, Log, TEXT("Output:\n%s"), *Results);
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
// Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editor only console commands.
|
||||||
|
*
|
||||||
|
* Such commands can be executed from the editor output log window, but also from command line arguments,
|
||||||
|
* from Editor Blueprints utilities, or from C++ Code using. eg. GEngine->Exec("git status Content/");
|
||||||
|
*/
|
||||||
|
class GitSourceControlConsole
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Git Command Line Interface: Run 'git' commands directly from the Unreal Editor Console.
|
||||||
|
static void ExecuteGitConsoleCommand(const TArray<FString>& a_args);
|
||||||
|
};
|
@ -0,0 +1,614 @@
|
|||||||
|
// 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"
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
|
||||||
|
#include "Styling/AppStyle.h"
|
||||||
|
#else
|
||||||
|
#include "EditorStyleSet.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "PackageTools.h"
|
||||||
|
#include "FileHelpers.h"
|
||||||
|
|
||||||
|
#include "Logging/MessageLog.h"
|
||||||
|
#include "SourceControlHelpers.h"
|
||||||
|
#include "SourceControlWindows.h"
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5
|
||||||
|
#include "ToolMenus.h"
|
||||||
|
#include "ToolMenuContext.h"
|
||||||
|
#include "ToolMenuMisc.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "UObject/Linker.h"
|
||||||
|
|
||||||
|
static const FName GitSourceControlMenuTabName(TEXT("GitSourceControlMenu"));
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "GitSourceControl"
|
||||||
|
|
||||||
|
TWeakPtr<SNotificationItem> FGitSourceControlMenu::OperationInProgressNotification;
|
||||||
|
|
||||||
|
void FGitSourceControlMenu::Register()
|
||||||
|
{
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
FToolMenuOwnerScoped SourceControlMenuOwner("GitSourceControlMenu");
|
||||||
|
if (UToolMenus* ToolMenus = UToolMenus::Get())
|
||||||
|
{
|
||||||
|
UToolMenu* SourceControlMenu = ToolMenus->ExtendMenu("StatusBar.ToolBar.SourceControl");
|
||||||
|
FToolMenuSection& Section = SourceControlMenu->AddSection("GitSourceControlActions", LOCTEXT("GitSourceControlMenuHeadingActions", "Git"), FToolMenuInsert(NAME_None, EToolMenuInsertType::First));
|
||||||
|
|
||||||
|
AddMenuExtension(Section);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// Register the extension with the level editor
|
||||||
|
FLevelEditorModule* LevelEditorModule = FModuleManager::GetModulePtr<FLevelEditorModule>(TEXT("LevelEditor"));
|
||||||
|
if (LevelEditorModule)
|
||||||
|
{
|
||||||
|
FLevelEditorModule::FLevelEditorMenuExtender ViewMenuExtender = FLevelEditorModule::FLevelEditorMenuExtender::CreateRaw(this, &FGitSourceControlMenu::OnExtendLevelEditorViewMenu);
|
||||||
|
auto& MenuExtenders = LevelEditorModule->GetAllLevelEditorToolbarSourceControlMenuExtenders();
|
||||||
|
MenuExtenders.Add(ViewMenuExtender);
|
||||||
|
ViewMenuExtenderHandle = MenuExtenders.Last().GetHandle();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlMenu::Unregister()
|
||||||
|
{
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
if (UToolMenus* ToolMenus = UToolMenus::Get())
|
||||||
|
{
|
||||||
|
UToolMenus::Get()->UnregisterOwnerByName("GitSourceControlMenu");
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// Unregister the level editor extensions
|
||||||
|
FLevelEditorModule* LevelEditorModule = FModuleManager::GetModulePtr<FLevelEditorModule>("LevelEditor");
|
||||||
|
if (LevelEditorModule)
|
||||||
|
{
|
||||||
|
LevelEditorModule->GetAllLevelEditorToolbarSourceControlMenuExtenders().RemoveAll([=](const FLevelEditorModule::FLevelEditorMenuExtender& Extender) { return Extender.GetHandle() == ViewMenuExtenderHandle; });
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlMenu::HaveRemoteUrl() const
|
||||||
|
{
|
||||||
|
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
return !GitSourceControl.GetProvider().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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask the user if they want to stash any modification and try to unstash them afterward, which could lead to conflicts
|
||||||
|
bool FGitSourceControlMenu::StashAwayAnyModifications()
|
||||||
|
{
|
||||||
|
bool bStashOk = true;
|
||||||
|
|
||||||
|
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
const FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
|
||||||
|
const FString& PathToRespositoryRoot = Provider.GetPathToRepositoryRoot();
|
||||||
|
const FString& PathToGitBinary = Provider.GetGitBinaryPath();
|
||||||
|
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, FGitSourceControlModule::GetEmptyStringArray(), 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, FGitSourceControlModule::GetEmptyStringArray(), 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 = FGitSourceControlModule::Get();
|
||||||
|
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
|
||||||
|
const FString& PathToRespositoryRoot = Provider.GetPathToRepositoryRoot();
|
||||||
|
const FString& PathToGitBinary = Provider.GetGitBinaryPath();
|
||||||
|
const TArray<FString> ParametersStash{ "pop" };
|
||||||
|
TArray<FString> InfoMessages;
|
||||||
|
TArray<FString> ErrorMessages;
|
||||||
|
const bool bUnstashOk = GitSourceControlUtils::RunCommand(TEXT("stash"), PathToGitBinary, PathToRespositoryRoot, ParametersStash, FGitSourceControlModule::GetEmptyStringArray(), 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)
|
||||||
|
{
|
||||||
|
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
|
||||||
|
|
||||||
|
// Launch a "Sync" operation
|
||||||
|
TSharedRef<FSync, ESPMode::ThreadSafe> SyncOperation = ISourceControlOperation::Create<FSync>();
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
const ECommandResult::Type Result = Provider.Execute(SyncOperation, FSourceControlChangelistPtr(), FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous,
|
||||||
|
FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete));
|
||||||
|
#else
|
||||||
|
const ECommandResult::Type Result = Provider.Execute(SyncOperation, FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous,
|
||||||
|
FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete));
|
||||||
|
#endif
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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", "Revision control operation already in progress"));
|
||||||
|
SourceControlLog.Notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlMenu::CommitClicked()
|
||||||
|
{
|
||||||
|
if (OperationInProgressNotification.IsValid())
|
||||||
|
{
|
||||||
|
FMessageLog SourceControlLog("SourceControl");
|
||||||
|
SourceControlLog.Warning(LOCTEXT("SourceControlMenu_InProgress", "Revision control operation already in progress"));
|
||||||
|
SourceControlLog.Notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FLevelEditorModule & LevelEditorModule = FModuleManager::Get().LoadModuleChecked<FLevelEditorModule>("LevelEditor");
|
||||||
|
FSourceControlWindows::ChoosePackagesToCheckIn(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlMenu::PushClicked()
|
||||||
|
{
|
||||||
|
if (!OperationInProgressNotification.IsValid())
|
||||||
|
{
|
||||||
|
// Launch a "Push" Operation
|
||||||
|
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
|
||||||
|
TSharedRef<FCheckIn, ESPMode::ThreadSafe> PushOperation = ISourceControlOperation::Create<FCheckIn>();
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
const ECommandResult::Type Result = Provider.Execute(PushOperation, FSourceControlChangelistPtr(), FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete));
|
||||||
|
#else
|
||||||
|
const ECommandResult::Type Result = Provider.Execute(PushOperation, FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete));
|
||||||
|
#endif
|
||||||
|
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", "Revision control operation already in progress"));
|
||||||
|
SourceControlLog.Notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlMenu::RevertClicked()
|
||||||
|
{
|
||||||
|
if (OperationInProgressNotification.IsValid())
|
||||||
|
{
|
||||||
|
FMessageLog SourceControlLog("SourceControl");
|
||||||
|
SourceControlLog.Warning(LOCTEXT("SourceControlMenu_InProgress", "Revision control operation already in progress"));
|
||||||
|
SourceControlLog.Notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we update the SCC status of all packages (this could take a long time, so we will run it as a background task)
|
||||||
|
const TArray<FString> Filenames {
|
||||||
|
FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()),
|
||||||
|
FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir()),
|
||||||
|
FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath())
|
||||||
|
};
|
||||||
|
|
||||||
|
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
|
||||||
|
FSourceControlOperationRef Operation = ISourceControlOperation::Create<FUpdateStatus>();
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
SourceControlProvider.Execute(Operation, FSourceControlChangelistPtr(), Filenames, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateStatic(&FGitSourceControlMenu::RevertAllCallback));
|
||||||
|
#else
|
||||||
|
SourceControlProvider.Execute(Operation, Filenames, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateStatic(&FGitSourceControlMenu::RevertAllCallback));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
FNotificationInfo Info(LOCTEXT("SourceControlMenuRevertAll", "Checking for assets to revert..."));
|
||||||
|
Info.bFireAndForget = false;
|
||||||
|
Info.ExpireDuration = 0.0f;
|
||||||
|
Info.FadeOutDuration = 1.0f;
|
||||||
|
|
||||||
|
if (SourceControlProvider.CanCancelOperation(Operation))
|
||||||
|
{
|
||||||
|
Info.ButtonDetails.Add(FNotificationButtonInfo(
|
||||||
|
LOCTEXT("SourceControlMenuRevertAll_CancelButton", "Cancel"),
|
||||||
|
LOCTEXT("SourceControlMenuRevertAll_CancelButtonTooltip", "Cancel the revert operation."),
|
||||||
|
FSimpleDelegate::CreateStatic(&FGitSourceControlMenu::RevertAllCancelled, Operation)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
OperationInProgressNotification = FSlateNotificationManager::Get().AddNotification(Info);
|
||||||
|
if (OperationInProgressNotification.IsValid())
|
||||||
|
{
|
||||||
|
OperationInProgressNotification.Pin()->SetCompletionState(SNotificationItem::CS_Pending);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlMenu::RevertAllCallback(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult)
|
||||||
|
{
|
||||||
|
if (InResult != ECommandResult::Succeeded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a list of all the checked out packages
|
||||||
|
TArray<FString> PackageNames;
|
||||||
|
TArray<UPackage*> LoadedPackages;
|
||||||
|
TMap<FString, FSourceControlStatePtr> PackageStates;
|
||||||
|
FEditorFileUtils::FindAllSubmittablePackageFiles(PackageStates, true);
|
||||||
|
|
||||||
|
for (TMap<FString, FSourceControlStatePtr>::TConstIterator PackageIter(PackageStates); PackageIter; ++PackageIter)
|
||||||
|
{
|
||||||
|
const FString PackageName = *PackageIter.Key();
|
||||||
|
const FSourceControlStatePtr CurPackageSCCState = PackageIter.Value();
|
||||||
|
|
||||||
|
UPackage* Package = FindPackage(nullptr, *PackageName);
|
||||||
|
if (Package != nullptr)
|
||||||
|
{
|
||||||
|
LoadedPackages.Add(Package);
|
||||||
|
|
||||||
|
if (!Package->IsFullyLoaded())
|
||||||
|
{
|
||||||
|
FlushAsyncLoading();
|
||||||
|
Package->FullyLoad();
|
||||||
|
}
|
||||||
|
ResetLoaders(Package);
|
||||||
|
}
|
||||||
|
|
||||||
|
PackageNames.Add(PackageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto FileNames = SourceControlHelpers::PackageFilenames(PackageNames);
|
||||||
|
|
||||||
|
// Launch a "Revert" Operation
|
||||||
|
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
|
||||||
|
const TSharedRef<FRevert, ESPMode::ThreadSafe> RevertOperation = ISourceControlOperation::Create<FRevert>();
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
const auto Result = Provider.Execute(RevertOperation, FSourceControlChangelistPtr(), FileNames);
|
||||||
|
#else
|
||||||
|
const auto Result = Provider.Execute(RevertOperation, FileNames);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
RemoveInProgressNotification();
|
||||||
|
if (Result != ECommandResult::Succeeded)
|
||||||
|
{
|
||||||
|
DisplayFailureNotification(TEXT("Revert"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DisplaySucessNotification(TEXT("Revert"));
|
||||||
|
}
|
||||||
|
|
||||||
|
GitSourceControlUtils::ReloadPackages(LoadedPackages);
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
Provider.Execute(ISourceControlOperation::Create<FUpdateStatus>(), FSourceControlChangelistPtr(), FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous);
|
||||||
|
#else
|
||||||
|
Provider.Execute(ISourceControlOperation::Create<FUpdateStatus>(), FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlMenu::RefreshClicked()
|
||||||
|
{
|
||||||
|
if (!OperationInProgressNotification.IsValid())
|
||||||
|
{
|
||||||
|
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
|
||||||
|
// Launch an "GitFetch" Operation
|
||||||
|
TSharedRef<FGitFetch, ESPMode::ThreadSafe> RefreshOperation = ISourceControlOperation::Create<FGitFetch>();
|
||||||
|
RefreshOperation->bUpdateStatus = true;
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
const ECommandResult::Type Result = Provider.Execute(RefreshOperation, FSourceControlChangelistPtr(), FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous,
|
||||||
|
FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete));
|
||||||
|
#else
|
||||||
|
const ECommandResult::Type Result = Provider.Execute(RefreshOperation, FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous,
|
||||||
|
FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete));
|
||||||
|
#endif
|
||||||
|
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", "Revision 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlMenu::RevertAllCancelled(FSourceControlOperationRef InOperation)
|
||||||
|
{
|
||||||
|
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
|
||||||
|
SourceControlProvider.CancelOperation(InOperation);
|
||||||
|
|
||||||
|
if (OperationInProgressNotification.IsValid())
|
||||||
|
{
|
||||||
|
OperationInProgressNotification.Pin()->ExpireAndFadeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
OperationInProgressNotification.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
|
||||||
|
Info.Image = FAppStyle::GetBrush(TEXT("NotificationList.SuccessImage"));
|
||||||
|
#else
|
||||||
|
Info.Image = FEditorStyle::GetBrush(TEXT("NotificationList.SuccessImage"));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
FSlateNotificationManager::Get().AddNotification(Info);
|
||||||
|
#if UE_BUILD_DEBUG
|
||||||
|
UE_LOG(LogSourceControl, Log, TEXT("%s"), *NotificationText.ToString());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
GitSourceControlUtils::ReloadPackages(PackagesToReload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report result with a notification
|
||||||
|
if (InResult == ECommandResult::Succeeded)
|
||||||
|
{
|
||||||
|
DisplaySucessNotification(InOperation->GetName());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DisplayFailureNotification(InOperation->GetName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
void FGitSourceControlMenu::AddMenuExtension(FToolMenuSection& Builder)
|
||||||
|
#else
|
||||||
|
void FGitSourceControlMenu::AddMenuExtension(FMenuBuilder& Builder)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Builder.AddMenuEntry(
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
"GitPush",
|
||||||
|
#endif
|
||||||
|
LOCTEXT("GitPush", "Push pending local commits"),
|
||||||
|
LOCTEXT("GitPushTooltip", "Push all pending local commits to the remote server."),
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
|
||||||
|
FSlateIcon(FAppStyle::GetAppStyleSetName(), "SourceControl.Actions.Submit"),
|
||||||
|
#else
|
||||||
|
FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Submit"),
|
||||||
|
#endif
|
||||||
|
FUIAction(
|
||||||
|
FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::PushClicked),
|
||||||
|
FCanExecuteAction::CreateRaw(this, &FGitSourceControlMenu::HaveRemoteUrl)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Builder.AddMenuEntry(
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
"GitSync",
|
||||||
|
#endif
|
||||||
|
LOCTEXT("GitSync", "Pull"),
|
||||||
|
LOCTEXT("GitSyncTooltip", "Update all files in the local repository to the latest version of the remote server."),
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
|
||||||
|
FSlateIcon(FAppStyle::GetAppStyleSetName(), "SourceControl.Actions.Sync"),
|
||||||
|
#else
|
||||||
|
FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Sync"),
|
||||||
|
#endif
|
||||||
|
FUIAction(
|
||||||
|
FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::SyncClicked),
|
||||||
|
FCanExecuteAction::CreateRaw(this, &FGitSourceControlMenu::HaveRemoteUrl)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Builder.AddMenuEntry(
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
"GitRevert",
|
||||||
|
#endif
|
||||||
|
LOCTEXT("GitRevert", "Revert"),
|
||||||
|
LOCTEXT("GitRevertTooltip", "Revert all files in the repository to their unchanged state."),
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
|
||||||
|
FSlateIcon(FAppStyle::GetAppStyleSetName(), "SourceControl.Actions.Revert"),
|
||||||
|
#else
|
||||||
|
FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Revert"),
|
||||||
|
#endif
|
||||||
|
FUIAction(
|
||||||
|
FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::RevertClicked),
|
||||||
|
FCanExecuteAction()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Builder.AddMenuEntry(
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
"GitRefresh",
|
||||||
|
#endif
|
||||||
|
LOCTEXT("GitRefresh", "Refresh"),
|
||||||
|
LOCTEXT("GitRefreshTooltip", "Update the revision control status of all files in the local repository."),
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
|
||||||
|
FSlateIcon(FAppStyle::GetAppStyleSetName(), "SourceControl.Actions.Refresh"),
|
||||||
|
#else
|
||||||
|
FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Refresh"),
|
||||||
|
#endif
|
||||||
|
FUIAction(
|
||||||
|
FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::RefreshClicked),
|
||||||
|
FCanExecuteAction()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION < 5
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
@ -0,0 +1,69 @@
|
|||||||
|
// 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 "Runtime/Launch/Resources/Version.h"
|
||||||
|
|
||||||
|
struct FToolMenuSection;
|
||||||
|
class FMenuBuilder;
|
||||||
|
|
||||||
|
/** Git extension of the Revision Control toolbar menu */
|
||||||
|
class FGitSourceControlMenu
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void Register();
|
||||||
|
void Unregister();
|
||||||
|
|
||||||
|
/** This functions will be bound to appropriate Command. */
|
||||||
|
void CommitClicked();
|
||||||
|
void PushClicked();
|
||||||
|
void SyncClicked();
|
||||||
|
void RevertClicked();
|
||||||
|
void RefreshClicked();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
static void RevertAllCallback(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult);
|
||||||
|
static void RevertAllCancelled(FSourceControlOperationRef InOperation);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool HaveRemoteUrl() const;
|
||||||
|
|
||||||
|
bool SaveDirtyPackages();
|
||||||
|
|
||||||
|
bool StashAwayAnyModifications();
|
||||||
|
void ReApplyStashedModifications();
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
void AddMenuExtension(FToolMenuSection& Builder);
|
||||||
|
#else
|
||||||
|
void AddMenuExtension(FMenuBuilder& Builder);
|
||||||
|
TSharedRef<class FExtender> OnExtendLevelEditorViewMenu(const TSharedRef<class FUICommandList> CommandList);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static void DisplayInProgressNotification(const FText& InOperationInProgressString);
|
||||||
|
static void RemoveInProgressNotification();
|
||||||
|
static void DisplaySucessNotification(const FName& InOperationName);
|
||||||
|
static void DisplayFailureNotification(const FName& InOperationName);
|
||||||
|
|
||||||
|
private:
|
||||||
|
#if ENGINE_MAJOR_VERSION < 5
|
||||||
|
FDelegateHandle ViewMenuExtenderHandle;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** 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 revision control operation from extended menu if any */
|
||||||
|
static TWeakPtr<class SNotificationItem> OperationInProgressNotification;
|
||||||
|
|
||||||
|
/** Delegate called when a revision control operation has completed */
|
||||||
|
void OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult);
|
||||||
|
};
|
@ -0,0 +1,241 @@
|
|||||||
|
// 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 "AssetToolsModule.h"
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
|
||||||
|
#include "Styling/AppStyle.h"
|
||||||
|
#else
|
||||||
|
#include "EditorStyleSet.h"
|
||||||
|
#endif
|
||||||
|
#include "Misc/App.h"
|
||||||
|
#include "Modules/ModuleManager.h"
|
||||||
|
#include "Features/IModularFeatures.h"
|
||||||
|
|
||||||
|
#include "ContentBrowserModule.h"
|
||||||
|
#include "ContentBrowserDelegates.h"
|
||||||
|
|
||||||
|
#include "GitSourceControlOperations.h"
|
||||||
|
#include "GitSourceControlUtils.h"
|
||||||
|
#include "ISourceControlModule.h"
|
||||||
|
#include "SourceControlHelpers.h"
|
||||||
|
#include "Framework/Commands/UIAction.h"
|
||||||
|
#include "Framework/MultiBox/MultiBoxExtender.h"
|
||||||
|
#include "Framework/MultiBox/MultiBoxBuilder.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "GitSourceControl"
|
||||||
|
|
||||||
|
TArray<FString> FGitSourceControlModule::EmptyStringArray;
|
||||||
|
|
||||||
|
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( "Fetch", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitFetchWorker> ) );
|
||||||
|
GitSourceControlProvider.RegisterWorker( "CheckIn", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitCheckInWorker> ) );
|
||||||
|
GitSourceControlProvider.RegisterWorker( "Copy", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitCopyWorker> ) );
|
||||||
|
GitSourceControlProvider.RegisterWorker( "Resolve", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitResolveWorker> ) );
|
||||||
|
GitSourceControlProvider.RegisterWorker( "MoveToChangelist", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitMoveToChangelistWorker> ) );
|
||||||
|
GitSourceControlProvider.RegisterWorker( "UpdateChangelistsStatus", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitUpdateStagingWorker> ) );
|
||||||
|
|
||||||
|
// load our settings
|
||||||
|
GitSourceControlSettings.LoadSettings();
|
||||||
|
|
||||||
|
// Bind our revision control provider to the editor
|
||||||
|
IModularFeatures::Get().RegisterModularFeature( "SourceControl", &GitSourceControlProvider );
|
||||||
|
|
||||||
|
FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked<FContentBrowserModule>("ContentBrowser");
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
// Register ContentBrowserDelegate Handles for UE5 EA
|
||||||
|
// At the time of writing this UE5 is in Early Access and has no support for revision control yet. So instead we hook into the content browser..
|
||||||
|
// .. and force a state update on the next tick for revision control. Usually the contentbrowser assets will request this themselves, but that's not working
|
||||||
|
// Values here are 1 or 2 based on whether the change can be done immediately or needs to be delayed as unreal needs to work through its internal delegates first
|
||||||
|
// >> Technically you wouldn't need to use `GetOnAssetSelectionChanged` -- but it's there as a safety mechanism. States aren't forceupdated for the first path that loads
|
||||||
|
// >> Making sure we force an update on selection change that acts like a just in case other measures fail
|
||||||
|
CbdHandle_OnFilterChanged = ContentBrowserModule.GetOnFilterChanged().AddLambda( [this]( const FARFilter&, bool ) { GitSourceControlProvider.TicksUntilNextForcedUpdate = 2; } );
|
||||||
|
CbdHandle_OnSearchBoxChanged = ContentBrowserModule.GetOnSearchBoxChanged().AddLambda( [this]( const FText&, bool ){ GitSourceControlProvider.TicksUntilNextForcedUpdate = 1; } );
|
||||||
|
CbdHandle_OnAssetSelectionChanged = ContentBrowserModule.GetOnAssetSelectionChanged().AddLambda( [this]( const TArray<FAssetData>&, bool ) { GitSourceControlProvider.TicksUntilNextForcedUpdate = 1; } );
|
||||||
|
CbdHandle_OnAssetPathChanged = ContentBrowserModule.GetOnAssetPathChanged().AddLambda( [this]( const FString& ) { GitSourceControlProvider.TicksUntilNextForcedUpdate = 2; } );
|
||||||
|
#endif
|
||||||
|
|
||||||
|
TArray<FContentBrowserMenuExtender_SelectedAssets>& CBAssetMenuExtenderDelegates = ContentBrowserModule.GetAllAssetViewContextMenuExtenders();
|
||||||
|
CBAssetMenuExtenderDelegates.Add(FContentBrowserMenuExtender_SelectedAssets::CreateRaw( this, &FGitSourceControlModule::OnExtendContentBrowserAssetSelectionMenu ));
|
||||||
|
CbdHandle_OnExtendAssetSelectionMenu = CBAssetMenuExtenderDelegates.Last().GetHandle();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlModule::ShutdownModule()
|
||||||
|
{
|
||||||
|
// shut down the provider, as this module is going away
|
||||||
|
GitSourceControlProvider.Close();
|
||||||
|
|
||||||
|
// unbind provider from editor
|
||||||
|
IModularFeatures::Get().UnregisterModularFeature("SourceControl", &GitSourceControlProvider);
|
||||||
|
|
||||||
|
|
||||||
|
// Unregister ContentBrowserDelegate Handles
|
||||||
|
FContentBrowserModule & ContentBrowserModule = FModuleManager::Get().LoadModuleChecked< FContentBrowserModule >( "ContentBrowser" );
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
ContentBrowserModule.GetOnFilterChanged().Remove( CbdHandle_OnFilterChanged );
|
||||||
|
ContentBrowserModule.GetOnSearchBoxChanged().Remove( CbdHandle_OnSearchBoxChanged );
|
||||||
|
ContentBrowserModule.GetOnAssetSelectionChanged().Remove( CbdHandle_OnAssetSelectionChanged );
|
||||||
|
ContentBrowserModule.GetOnAssetPathChanged().Remove( CbdHandle_OnAssetPathChanged );
|
||||||
|
#endif
|
||||||
|
|
||||||
|
TArray<FContentBrowserMenuExtender_SelectedAssets>& CBAssetMenuExtenderDelegates = ContentBrowserModule.GetAllAssetViewContextMenuExtenders();
|
||||||
|
CBAssetMenuExtenderDelegates.RemoveAll([ &ExtenderDelegateHandle = CbdHandle_OnExtendAssetSelectionMenu ]( const FContentBrowserMenuExtender_SelectedAssets& Delegate ) {
|
||||||
|
return Delegate.GetHandle() == ExtenderDelegateHandle;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlModule::SaveSettings()
|
||||||
|
{
|
||||||
|
if (FApp::IsUnattended() || IsRunningCommandlet())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GitSourceControlSettings.SaveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlModule::SetLastErrors(const TArray<FText>& InErrors)
|
||||||
|
{
|
||||||
|
FGitSourceControlModule* Module = FModuleManager::GetModulePtr<FGitSourceControlModule>("GitSourceControl");
|
||||||
|
if (Module)
|
||||||
|
{
|
||||||
|
Module->GetProvider().SetLastErrors(InErrors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedRef<FExtender> FGitSourceControlModule::OnExtendContentBrowserAssetSelectionMenu(const TArray<FAssetData>& SelectedAssets)
|
||||||
|
{
|
||||||
|
TSharedRef<FExtender> Extender(new FExtender());
|
||||||
|
|
||||||
|
Extender->AddMenuExtension(
|
||||||
|
"AssetSourceControlActions",
|
||||||
|
EExtensionHook::After,
|
||||||
|
nullptr,
|
||||||
|
FMenuExtensionDelegate::CreateRaw( this, &FGitSourceControlModule::CreateGitContentBrowserAssetMenu, SelectedAssets )
|
||||||
|
);
|
||||||
|
|
||||||
|
return Extender;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlModule::CreateGitContentBrowserAssetMenu(FMenuBuilder& MenuBuilder, const TArray<FAssetData> SelectedAssets)
|
||||||
|
{
|
||||||
|
if (!FGitSourceControlModule::Get().GetProvider().GetStatusBranchNames().Num())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TArray<FString>& StatusBranchNames = FGitSourceControlModule::Get().GetProvider().GetStatusBranchNames();
|
||||||
|
const FString& BranchName = StatusBranchNames[0];
|
||||||
|
MenuBuilder.AddMenuEntry(
|
||||||
|
FText::Format(LOCTEXT("StatusBranchDiff", "Diff against status branch"), FText::FromString(BranchName)),
|
||||||
|
FText::Format(LOCTEXT("StatusBranchDiffDesc", "Compare this asset to the latest status branch version"), FText::FromString(BranchName)),
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
|
||||||
|
FSlateIcon(FAppStyle::GetAppStyleSetName(), "SourceControl.Actions.Diff"),
|
||||||
|
#else
|
||||||
|
FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Diff"),
|
||||||
|
#endif
|
||||||
|
FUIAction(FExecuteAction::CreateRaw( this, &FGitSourceControlModule::DiffAssetAgainstGitOriginBranch, SelectedAssets, BranchName ))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlModule::DiffAssetAgainstGitOriginBranch(const TArray<FAssetData> SelectedAssets, FString BranchName) const
|
||||||
|
{
|
||||||
|
for (int32 AssetIdx = 0; AssetIdx < SelectedAssets.Num(); AssetIdx++)
|
||||||
|
{
|
||||||
|
// Get the actual asset (will load it)
|
||||||
|
const FAssetData& AssetData = SelectedAssets[AssetIdx];
|
||||||
|
|
||||||
|
if (UObject* CurrentObject = AssetData.GetAsset())
|
||||||
|
{
|
||||||
|
const FString PackagePath = AssetData.PackageName.ToString();
|
||||||
|
const FString PackageName = AssetData.AssetName.ToString();
|
||||||
|
DiffAgainstOriginBranch(CurrentObject, PackagePath, PackageName, BranchName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlModule::DiffAgainstOriginBranch( UObject * InObject, const FString & InPackagePath, const FString & InPackageName, const FString & BranchName ) const
|
||||||
|
{
|
||||||
|
check(InObject);
|
||||||
|
|
||||||
|
const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("GitSourceControl");
|
||||||
|
const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
|
||||||
|
const FString& PathToRepositoryRoot = GitSourceControl.GetProvider().GetPathToRepositoryRoot();
|
||||||
|
|
||||||
|
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
|
||||||
|
|
||||||
|
const FAssetToolsModule& AssetToolsModule = FModuleManager::GetModuleChecked<FAssetToolsModule>("AssetTools");
|
||||||
|
|
||||||
|
// Get the SCC state
|
||||||
|
const FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(SourceControlHelpers::PackageFilename(InPackagePath), EStateCacheUsage::Use);
|
||||||
|
|
||||||
|
// If we have an asset and its in SCC..
|
||||||
|
if (SourceControlState.IsValid() && InObject != nullptr && SourceControlState->IsSourceControlled())
|
||||||
|
{
|
||||||
|
// Get the file name of package
|
||||||
|
FString RelativeFileName;
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
if (FPackageName::DoesPackageExist(InPackagePath, &RelativeFileName))
|
||||||
|
#else
|
||||||
|
if (FPackageName::DoesPackageExist(InPackagePath, nullptr, &RelativeFileName))
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
// if(SourceControlState->GetHistorySize() > 0)
|
||||||
|
{
|
||||||
|
TArray<FString> Errors;
|
||||||
|
const auto& Revision = GitSourceControlUtils::GetOriginRevisionOnBranch(PathToGitBinary, PathToRepositoryRoot, RelativeFileName, Errors, BranchName);
|
||||||
|
|
||||||
|
check(Revision.IsValid());
|
||||||
|
|
||||||
|
FString TempFileName;
|
||||||
|
if (Revision->Get(TempFileName))
|
||||||
|
{
|
||||||
|
// Try and load that package
|
||||||
|
UPackage* TempPackage = LoadPackage(nullptr, *TempFileName, LOAD_ForDiff | LOAD_DisableCompileOnLoad);
|
||||||
|
if (TempPackage != nullptr)
|
||||||
|
{
|
||||||
|
// Grab the old asset from that old package
|
||||||
|
UObject* OldObject = FindObject<UObject>(TempPackage, *InPackageName);
|
||||||
|
if (OldObject != nullptr)
|
||||||
|
{
|
||||||
|
/* Set the revision information*/
|
||||||
|
FRevisionInfo OldRevision;
|
||||||
|
OldRevision.Changelist = Revision->GetCheckInIdentifier();
|
||||||
|
OldRevision.Date = Revision->GetDate();
|
||||||
|
OldRevision.Revision = Revision->GetRevision();
|
||||||
|
|
||||||
|
FRevisionInfo NewRevision;
|
||||||
|
NewRevision.Revision = TEXT("");
|
||||||
|
|
||||||
|
AssetToolsModule.Get().DiffAssets(OldObject, InObject, OldRevision, NewRevision);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_MODULE( FGitSourceControlModule, GitSourceControl );
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
@ -0,0 +1,160 @@
|
|||||||
|
// 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 "Modules/ModuleInterface.h"
|
||||||
|
#include "Modules/ModuleManager.h"
|
||||||
|
|
||||||
|
#include "GitSourceControlSettings.h"
|
||||||
|
#include "GitSourceControlProvider.h"
|
||||||
|
|
||||||
|
struct FAssetData;
|
||||||
|
class FExtender;
|
||||||
|
|
||||||
|
/**
|
||||||
|
|
||||||
|
UEGitPlugin is a simple Git Revision 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 UE 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:
|
||||||
|
|
||||||
|
Use "TODO LFS" in the code to track things left to do/improve/refactor:
|
||||||
|
2. Implement FGitSourceControlProvider::bWorkingOffline like the SubversionSourceControl plugin
|
||||||
|
3. Trying to deactivate Git LFS 2 file locking afterward on the "Login to Revision Control" (Connect/Configure) screen
|
||||||
|
is not working after Git LFS 2 has switched "read-only" flag on files (which needs the Checkout operation to be editable)!
|
||||||
|
- temporarily deactivating locks may be required if we want to be able to work while not connected (do we really need this ???)
|
||||||
|
- does Git LFS have a command to do this deactivation ?
|
||||||
|
- perhaps should we rely on detection of such flags to detect LFS 2 usage (ie. the need to do a checkout)
|
||||||
|
- see SubversionSourceControl plugin that deals with such flags
|
||||||
|
- this would need a rework of the way the "bIsUsingFileLocking" is propagated, since this would no more be a configuration (or not only) but a file state
|
||||||
|
- else we should at least revert those read-only flags when going out of "Lock mode"
|
||||||
|
|
||||||
|
### What *cannot* be done presently
|
||||||
|
- Branch/Merge are 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
|
||||||
|
- renaming a Blueprint in Editor leaves a redirector file, AND modify too much the asset to enable git to track its history through renaming
|
||||||
|
- standard Editor commit dialog asks if user wants to "Keep Files Checked Out" => no use for Git or Mercurial CanCheckOut()==false
|
||||||
|
*/
|
||||||
|
class FGitSourceControlModule : public IModuleInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/** IModuleInterface implementation */
|
||||||
|
virtual void StartupModule() override;
|
||||||
|
virtual void ShutdownModule() override;
|
||||||
|
|
||||||
|
/** Access the Git revision control settings */
|
||||||
|
FGitSourceControlSettings& AccessSettings()
|
||||||
|
{
|
||||||
|
return GitSourceControlSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FGitSourceControlSettings& AccessSettings() const
|
||||||
|
{
|
||||||
|
return GitSourceControlSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Save the Git revision control settings */
|
||||||
|
void SaveSettings();
|
||||||
|
|
||||||
|
/** Access the Git revision control provider */
|
||||||
|
FGitSourceControlProvider& GetProvider()
|
||||||
|
{
|
||||||
|
return GitSourceControlProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FGitSourceControlProvider& GetProvider() const
|
||||||
|
{
|
||||||
|
return GitSourceControlProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const TArray<FString>& GetEmptyStringArray()
|
||||||
|
{
|
||||||
|
return EmptyStringArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton-like access to this module's interface. This is just for convenience!
|
||||||
|
* Beware of calling this during the shutdown phase, though. Your module might have been unloaded already.
|
||||||
|
*
|
||||||
|
* @return Returns singleton instance, loading the module on demand if needed
|
||||||
|
*/
|
||||||
|
static inline FGitSourceControlModule& Get()
|
||||||
|
{
|
||||||
|
return FModuleManager::Get().LoadModuleChecked< FGitSourceControlModule >("GitSourceControl");
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline FGitSourceControlModule* GetThreadSafe()
|
||||||
|
{
|
||||||
|
IModuleInterface* ModulePtr = FModuleManager::Get().GetModule("GitSourceControl");
|
||||||
|
if (!ModulePtr)
|
||||||
|
{
|
||||||
|
// Main thread should never have this unloaded.
|
||||||
|
check(!IsInGameThread());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return static_cast<FGitSourceControlModule*>(ModulePtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set list of error messages that occurred after last git command */
|
||||||
|
static void SetLastErrors(const TArray<FText>& InErrors);
|
||||||
|
|
||||||
|
private:
|
||||||
|
TSharedRef<FExtender> OnExtendContentBrowserAssetSelectionMenu(const TArray<FAssetData>& SelectedAssets);
|
||||||
|
void CreateGitContentBrowserAssetMenu(FMenuBuilder& MenuBuilder, const TArray<FAssetData> SelectedAssets);
|
||||||
|
void DiffAssetAgainstGitOriginBranch(const TArray<FAssetData> SelectedAssets, FString BranchName) const;
|
||||||
|
void DiffAgainstOriginBranch(UObject* InObject, const FString& InPackagePath, const FString& InPackageName, const FString& BranchName) const;
|
||||||
|
|
||||||
|
/** The one and only Git revision control provider */
|
||||||
|
FGitSourceControlProvider GitSourceControlProvider;
|
||||||
|
|
||||||
|
/** The settings for Git revision control */
|
||||||
|
FGitSourceControlSettings GitSourceControlSettings;
|
||||||
|
|
||||||
|
static TArray<FString> EmptyStringArray;
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
// ContentBrowserDelegate Handles
|
||||||
|
FDelegateHandle CbdHandle_OnFilterChanged;
|
||||||
|
FDelegateHandle CbdHandle_OnSearchBoxChanged;
|
||||||
|
FDelegateHandle CbdHandle_OnAssetSelectionChanged;
|
||||||
|
FDelegateHandle CbdHandle_OnSourcesViewChanged;
|
||||||
|
FDelegateHandle CbdHandle_OnAssetPathChanged;
|
||||||
|
#endif
|
||||||
|
FDelegateHandle CbdHandle_OnExtendAssetSelectionMenu;
|
||||||
|
};
|
@ -0,0 +1,909 @@
|
|||||||
|
// 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 "SourceControlHelpers.h"
|
||||||
|
#include "Logging/MessageLog.h"
|
||||||
|
#include "Misc/MessageDialog.h"
|
||||||
|
#include "HAL/PlatformProcess.h"
|
||||||
|
#include "GenericPlatform/GenericPlatformFile.h"
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
#include "HAL/PlatformFileManager.h"
|
||||||
|
#else
|
||||||
|
#include "HAL/PlatformFilemanager.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "GitSourceControl"
|
||||||
|
|
||||||
|
FName FGitConnectWorker::GetName() const
|
||||||
|
{
|
||||||
|
return "Connect";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitConnectWorker::Execute(FGitSourceControlCommand& InCommand)
|
||||||
|
{
|
||||||
|
// The connect worker checks if we are connected to the remote server.
|
||||||
|
check(InCommand.Operation->GetName() == GetName());
|
||||||
|
TSharedRef<FConnect, ESPMode::ThreadSafe> Operation = StaticCastSharedRef<FConnect>(InCommand.Operation);
|
||||||
|
|
||||||
|
// Skip login operations, since Git does not have to login.
|
||||||
|
// It's not a big deal for async commands though, so let those go through.
|
||||||
|
// More information: this is a heuristic for cases where UE is trying to create
|
||||||
|
// a valid Perforce connection as a side effect for the connect worker. For Git,
|
||||||
|
// the connect worker has no side effects. It is simply a query to retrieve information
|
||||||
|
// to be displayed to the user, like in the revision control settings or on init.
|
||||||
|
// Therefore, there is no need for synchronously establishing a connection if not there.
|
||||||
|
if (InCommand.Concurrency == EConcurrency::Synchronous)
|
||||||
|
{
|
||||||
|
InCommand.bCommandSuccessful = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Git availability
|
||||||
|
// We already know that Git is available if PathToGitBinary is not empty, since it is validated then.
|
||||||
|
if (InCommand.PathToGitBinary.IsEmpty())
|
||||||
|
{
|
||||||
|
const FText& NotFound = LOCTEXT("GitNotFound", "Failed to enable Git revision control. You need to install Git and ensure the plugin has a valid path to the git executable.");
|
||||||
|
InCommand.ResultInfo.ErrorMessages.Add(NotFound.ToString());
|
||||||
|
Operation->SetErrorText(NotFound);
|
||||||
|
InCommand.bCommandSuccessful = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get default branch: git remote show
|
||||||
|
|
||||||
|
TArray<FString> Parameters {
|
||||||
|
TEXT("-h"), // Only limit to branches
|
||||||
|
TEXT("-q") // Skip printing out remote URL, we don't use it
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if remote matches our refs.
|
||||||
|
// Could be useful in the future, but all we want to know right now is if connection is up.
|
||||||
|
// Parameters.Add("--exit-code");
|
||||||
|
TArray<FString> InfoMessages;
|
||||||
|
TArray<FString> ErrorMessages;
|
||||||
|
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("ls-remote"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages);
|
||||||
|
if (!InCommand.bCommandSuccessful)
|
||||||
|
{
|
||||||
|
const FText& NotFound = LOCTEXT("GitRemoteFailed", "Failed Git remote connection. Ensure your repo is initialized, and check your connection to the Git host.");
|
||||||
|
InCommand.ResultInfo.ErrorMessages.Add(NotFound.ToString());
|
||||||
|
Operation->SetErrorText(NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: always return true, and enter an offline mode if could not connect to remote
|
||||||
|
return InCommand.bCommandSuccessful;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitConnectWorker::UpdateStates() const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FName FGitCheckOutWorker::GetName() const
|
||||||
|
{
|
||||||
|
return "CheckOut";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitCheckOutWorker::Execute(FGitSourceControlCommand& InCommand)
|
||||||
|
{
|
||||||
|
// If we have nothing to process, exit immediately
|
||||||
|
if (InCommand.Files.Num() == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
check(InCommand.Operation->GetName() == GetName());
|
||||||
|
|
||||||
|
if (!InCommand.bUsingGitLfsLocking)
|
||||||
|
{
|
||||||
|
InCommand.bCommandSuccessful = false;
|
||||||
|
return InCommand.bCommandSuccessful;
|
||||||
|
}
|
||||||
|
|
||||||
|
// lock files: execute the LFS command on relative filenames
|
||||||
|
const TArray<FString>& RelativeFiles = GitSourceControlUtils::RelativeFilenames(InCommand.Files, InCommand.PathToGitRoot);
|
||||||
|
|
||||||
|
const TArray<FString>& LockableRelativeFiles = RelativeFiles.FilterByPredicate(GitSourceControlUtils::IsFileLFSLockable);
|
||||||
|
|
||||||
|
if (LockableRelativeFiles.Num() < 1)
|
||||||
|
{
|
||||||
|
InCommand.bCommandSuccessful = true;
|
||||||
|
return InCommand.bCommandSuccessful;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool bSuccess = GitSourceControlUtils::RunLFSCommand(TEXT("lock"), InCommand.PathToGitRoot, InCommand.PathToGitBinary, FGitSourceControlModule::GetEmptyStringArray(), LockableRelativeFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
InCommand.bCommandSuccessful = bSuccess;
|
||||||
|
const FString& LockUser = FGitSourceControlModule::Get().GetProvider().GetLockUser();
|
||||||
|
if (bSuccess)
|
||||||
|
{
|
||||||
|
TArray<FString> AbsoluteFiles;
|
||||||
|
for (const auto& RelativeFile : RelativeFiles)
|
||||||
|
{
|
||||||
|
FString AbsoluteFile = FPaths::Combine(InCommand.PathToGitRoot, RelativeFile);
|
||||||
|
FGitLockedFilesCache::AddLockedFile(AbsoluteFile, LockUser);
|
||||||
|
FPaths::NormalizeFilename(AbsoluteFile);
|
||||||
|
AbsoluteFiles.Add(AbsoluteFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
GitSourceControlUtils::CollectNewStates(AbsoluteFiles, States, EFileState::Unset, ETreeState::Unset, ELockState::Locked);
|
||||||
|
for (auto& State : States)
|
||||||
|
{
|
||||||
|
State.Value.LockUser = LockUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
FName FGitCheckInWorker::GetName() const
|
||||||
|
{
|
||||||
|
return "CheckIn";
|
||||||
|
}
|
||||||
|
|
||||||
|
const FText EmptyCommitMsg;
|
||||||
|
|
||||||
|
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
|
||||||
|
bool bDoCommit = InCommand.Files.Num() > 0;
|
||||||
|
const FText& CommitMsg = bDoCommit ? Operation->GetDescription() : EmptyCommitMsg;
|
||||||
|
FGitScopedTempFile CommitMsgFile(CommitMsg);
|
||||||
|
if (CommitMsgFile.GetFilename().Len() > 0)
|
||||||
|
{
|
||||||
|
FGitSourceControlProvider& Provider = FGitSourceControlModule::Get().GetProvider();
|
||||||
|
|
||||||
|
if (bDoCommit)
|
||||||
|
{
|
||||||
|
FString ParamCommitMsgFilename = TEXT("--file=\"");
|
||||||
|
ParamCommitMsgFilename += FPaths::ConvertRelativePathToFull(CommitMsgFile.GetFilename());
|
||||||
|
ParamCommitMsgFilename += TEXT("\"");
|
||||||
|
TArray<FString> CommitParameters {ParamCommitMsgFilename};
|
||||||
|
const TArray<FString>& FilesToCommit = GitSourceControlUtils::RelativeFilenames(InCommand.Files, InCommand.PathToRepositoryRoot);
|
||||||
|
|
||||||
|
// If no files were committed, this is false, so we treat it as if we never wanted to commit in the first place.
|
||||||
|
bDoCommit = GitSourceControlUtils::RunCommit(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, CommitParameters,
|
||||||
|
FilesToCommit, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we commit, we can push up the deleted state to gone
|
||||||
|
if (bDoCommit)
|
||||||
|
{
|
||||||
|
// Remove any deleted files from status cache
|
||||||
|
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.ResultInfo.InfoMessages));
|
||||||
|
const FString& Message = (InCommand.ResultInfo.InfoMessages.Num() > 0) ? InCommand.ResultInfo.InfoMessages[0] : TEXT("");
|
||||||
|
UE_LOG(LogSourceControl, Log, TEXT("commit successful: %s"), *Message);
|
||||||
|
GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect difference between the remote and what we have on top of remote locally. This is to handle unpushed commits other than the one we just did.
|
||||||
|
// Doesn't matter that we're not synced. Because our local branch is always based on the remote.
|
||||||
|
TArray<FString> CommittedFiles;
|
||||||
|
FString BranchName;
|
||||||
|
bool bDiffSuccess;
|
||||||
|
if (GitSourceControlUtils::GetRemoteBranchName(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, BranchName))
|
||||||
|
{
|
||||||
|
TArray<FString> Parameters {"--name-only", FString::Printf(TEXT("%s...HEAD"), *BranchName), "--"};
|
||||||
|
bDiffSuccess = GitSourceControlUtils::RunCommand(TEXT("diff"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters,
|
||||||
|
FGitSourceControlModule::GetEmptyStringArray(), CommittedFiles, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Get all non-remote commits and list out their files
|
||||||
|
TArray<FString> Parameters {"--branches", "--not" "--remotes", "--name-only", "--pretty="};
|
||||||
|
bDiffSuccess = GitSourceControlUtils::RunCommand(TEXT("log"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters, FGitSourceControlModule::GetEmptyStringArray(), CommittedFiles, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
// Dedup files list between commits
|
||||||
|
CommittedFiles = TSet<FString>{CommittedFiles}.Array();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bUnpushedFiles;
|
||||||
|
TSet<FString> FilesToCheckIn {InCommand.Files};
|
||||||
|
if (bDiffSuccess)
|
||||||
|
{
|
||||||
|
// Only push if we have a difference (any commits at all, not just the one we just did)
|
||||||
|
bUnpushedFiles = CommittedFiles.Num() > 0;
|
||||||
|
CommittedFiles = GitSourceControlUtils::AbsoluteFilenames(CommittedFiles, InCommand.PathToRepositoryRoot);
|
||||||
|
FilesToCheckIn.Append(CommittedFiles.FilterByPredicate(GitSourceControlUtils::IsFileLFSLockable));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Be cautious, try pushing anyway
|
||||||
|
bUnpushedFiles = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FString> PulledFiles;
|
||||||
|
|
||||||
|
// If we have unpushed files, push
|
||||||
|
if (bUnpushedFiles)
|
||||||
|
{
|
||||||
|
// TODO: configure remote
|
||||||
|
TArray<FString> PushParameters {TEXT("-u"), TEXT("origin"), TEXT("HEAD")};
|
||||||
|
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("push"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot,
|
||||||
|
PushParameters, FGitSourceControlModule::GetEmptyStringArray(),
|
||||||
|
InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
|
||||||
|
if (!InCommand.bCommandSuccessful)
|
||||||
|
{
|
||||||
|
// if out of date, pull first, then try again
|
||||||
|
bool bWasOutOfDate = false;
|
||||||
|
for (const auto& PushError : InCommand.ResultInfo.ErrorMessages)
|
||||||
|
{
|
||||||
|
if ((PushError.Contains(TEXT("[rejected]")) && (PushError.Contains(TEXT("non-fast-forward")) || PushError.Contains(TEXT("fetch first")))) ||
|
||||||
|
PushError.Contains(TEXT("cannot lock ref")))
|
||||||
|
{
|
||||||
|
// Don't do it during iteration, want to append pull results to InCommand.ResultInfo.ErrorMessages
|
||||||
|
bWasOutOfDate = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bWasOutOfDate)
|
||||||
|
{
|
||||||
|
// Get latest
|
||||||
|
const bool bFetched = GitSourceControlUtils::FetchRemote(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, false,
|
||||||
|
InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
if (bFetched)
|
||||||
|
{
|
||||||
|
// Update local with latest
|
||||||
|
const bool bPulled = GitSourceControlUtils::PullOrigin(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot,
|
||||||
|
FGitSourceControlModule::GetEmptyStringArray(), PulledFiles,
|
||||||
|
InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
if (bPulled)
|
||||||
|
{
|
||||||
|
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(
|
||||||
|
TEXT("push"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, PushParameters,
|
||||||
|
FGitSourceControlModule::GetEmptyStringArray(), InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our push still wasn't successful
|
||||||
|
if (!InCommand.bCommandSuccessful)
|
||||||
|
{
|
||||||
|
if (!Provider.bPendingRestart)
|
||||||
|
{
|
||||||
|
// If it fails, just let the user do it
|
||||||
|
FText PushFailMessage(LOCTEXT("GitPush_OutOfDate_Msg", "Git Push failed because there are changes you need to pull.\n\n"
|
||||||
|
"An attempt was made to pull, but failed, because while the Unreal Editor is "
|
||||||
|
"open, files cannot always be updated.\n\n"
|
||||||
|
"Please exit the editor, and update the project again."));
|
||||||
|
FText PushFailTitle(LOCTEXT("GitPush_OutOfDate_Title", "Git Pull Required"));
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
|
||||||
|
FMessageDialog::Open(EAppMsgType::Ok, PushFailMessage, PushFailTitle);
|
||||||
|
#else
|
||||||
|
FMessageDialog::Open(EAppMsgType::Ok, PushFailMessage, &PushFailTitle);
|
||||||
|
#endif
|
||||||
|
UE_LOG(LogSourceControl, Log, TEXT("Push failed because we're out of date, prompting user to resolve manually"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
InCommand.bCommandSuccessful = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// git-lfs: unlock files
|
||||||
|
if (InCommand.bUsingGitLfsLocking)
|
||||||
|
{
|
||||||
|
// If we successfully pushed (or didn't need to push), unlock the files marked for check in
|
||||||
|
if (InCommand.bCommandSuccessful)
|
||||||
|
{
|
||||||
|
// unlock files: execute the LFS command on relative filenames
|
||||||
|
// (unlock only locked files, that is, not Added files)
|
||||||
|
TArray<FString> LockedFiles;
|
||||||
|
GitSourceControlUtils::GetLockedFiles(FilesToCheckIn.Array(), LockedFiles);
|
||||||
|
if (LockedFiles.Num() > 0)
|
||||||
|
{
|
||||||
|
const TArray<FString>& FilesToUnlock = GitSourceControlUtils::RelativeFilenames(LockedFiles, InCommand.PathToGitRoot);
|
||||||
|
|
||||||
|
if (FilesToUnlock.Num() > 0)
|
||||||
|
{
|
||||||
|
// Not strictly necessary to succeed, so don't update command success
|
||||||
|
const bool bUnlockSuccess = GitSourceControlUtils::RunLFSCommand(TEXT("unlock"), InCommand.PathToGitRoot, InCommand.PathToGitBinary,
|
||||||
|
FGitSourceControlModule::GetEmptyStringArray(), FilesToUnlock,
|
||||||
|
InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
if (bUnlockSuccess)
|
||||||
|
{
|
||||||
|
for (const auto& File : LockedFiles)
|
||||||
|
{
|
||||||
|
FGitLockedFilesCache::RemoveLockedFile(File);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if 0
|
||||||
|
for (const FString& File : FilesToCheckIn.Array())
|
||||||
|
{
|
||||||
|
FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*File, true);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all the files we touched through the pull update
|
||||||
|
if (bUnpushedFiles && PulledFiles.Num())
|
||||||
|
{
|
||||||
|
FilesToCheckIn.Append(PulledFiles);
|
||||||
|
}
|
||||||
|
// Before, we added only lockable files from CommittedFiles. But now, we want to update all files, not just lockables.
|
||||||
|
FilesToCheckIn.Append(CommittedFiles);
|
||||||
|
|
||||||
|
// now update the status of our files
|
||||||
|
TMap<FString, FGitSourceControlState> UpdatedStates;
|
||||||
|
bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking,
|
||||||
|
FilesToCheckIn.Array(), InCommand.ResultInfo.ErrorMessages, UpdatedStates);
|
||||||
|
if (bSuccess)
|
||||||
|
{
|
||||||
|
GitSourceControlUtils::CollectNewStates(UpdatedStates, States);
|
||||||
|
}
|
||||||
|
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
|
||||||
|
return InCommand.bCommandSuccessful;
|
||||||
|
}
|
||||||
|
|
||||||
|
InCommand.bCommandSuccessful = false;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitCheckInWorker::UpdateStates() const
|
||||||
|
{
|
||||||
|
return GitSourceControlUtils::UpdateCachedStates(States);
|
||||||
|
}
|
||||||
|
|
||||||
|
FName FGitMarkForAddWorker::GetName() const
|
||||||
|
{
|
||||||
|
return "MarkForAdd";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitMarkForAddWorker::Execute(FGitSourceControlCommand& InCommand)
|
||||||
|
{
|
||||||
|
// If we have nothing to process, exit immediately
|
||||||
|
if (InCommand.Files.Num() == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
check(InCommand.Operation->GetName() == GetName());
|
||||||
|
|
||||||
|
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), InCommand.Files, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
|
||||||
|
if (InCommand.bCommandSuccessful)
|
||||||
|
{
|
||||||
|
GitSourceControlUtils::CollectNewStates(InCommand.Files, States, EFileState::Added, ETreeState::Staged);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TMap<FString, FGitSourceControlState> UpdatedStates;
|
||||||
|
bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates);
|
||||||
|
if (bSuccess)
|
||||||
|
{
|
||||||
|
GitSourceControlUtils::CollectNewStates(UpdatedStates, States);
|
||||||
|
}
|
||||||
|
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return InCommand.bCommandSuccessful;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitMarkForAddWorker::UpdateStates() const
|
||||||
|
{
|
||||||
|
return GitSourceControlUtils::UpdateCachedStates(States);
|
||||||
|
}
|
||||||
|
|
||||||
|
FName FGitDeleteWorker::GetName() const
|
||||||
|
{
|
||||||
|
return "Delete";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitDeleteWorker::Execute(FGitSourceControlCommand& InCommand)
|
||||||
|
{
|
||||||
|
// If we have nothing to process, exit immediately
|
||||||
|
if (InCommand.Files.Num() == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
check(InCommand.Operation->GetName() == GetName());
|
||||||
|
|
||||||
|
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("rm"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), InCommand.Files, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
|
||||||
|
if (InCommand.bCommandSuccessful)
|
||||||
|
{
|
||||||
|
GitSourceControlUtils::CollectNewStates(InCommand.Files, States, EFileState::Deleted, ETreeState::Staged);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TMap<FString, FGitSourceControlState> UpdatedStates;
|
||||||
|
bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates);
|
||||||
|
if (bSuccess)
|
||||||
|
{
|
||||||
|
GitSourceControlUtils::CollectNewStates(UpdatedStates, States);
|
||||||
|
}
|
||||||
|
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = FGitSourceControlModule::Get();
|
||||||
|
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 already queued for deletion, don't try to delete again
|
||||||
|
if (State->IsSourceControlled() && !State->IsDeleted())
|
||||||
|
{
|
||||||
|
OutMissingFiles.Add(State->GetFilename());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FName FGitRevertWorker::GetName() const
|
||||||
|
{
|
||||||
|
return "Revert";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitRevertWorker::Execute(FGitSourceControlCommand& InCommand)
|
||||||
|
{
|
||||||
|
InCommand.bCommandSuccessful = true;
|
||||||
|
|
||||||
|
// Filter files by status
|
||||||
|
TArray<FString> MissingFiles;
|
||||||
|
TArray<FString> AllExistingFiles;
|
||||||
|
TArray<FString> OtherThanAddedExistingFiles;
|
||||||
|
GetMissingVsExistingFiles(InCommand.Files, MissingFiles, AllExistingFiles, OtherThanAddedExistingFiles);
|
||||||
|
|
||||||
|
const bool bRevertAll = InCommand.Files.Num() < 1;
|
||||||
|
if (bRevertAll)
|
||||||
|
{
|
||||||
|
TArray<FString> Parms;
|
||||||
|
Parms.Add(TEXT("--hard"));
|
||||||
|
InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("reset"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parms, FGitSourceControlModule::GetEmptyStringArray(), InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
|
||||||
|
Parms.Reset(2);
|
||||||
|
Parms.Add(TEXT("-f")); // force
|
||||||
|
Parms.Add(TEXT("-d")); // remove directories
|
||||||
|
InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("clean"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parms, FGitSourceControlModule::GetEmptyStringArray(), InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (MissingFiles.Num() > 0)
|
||||||
|
{
|
||||||
|
// "Added" files that have been deleted needs to be removed from revision control
|
||||||
|
InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("rm"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), MissingFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
}
|
||||||
|
if (AllExistingFiles.Num() > 0)
|
||||||
|
{
|
||||||
|
// reset and revert any changes already added to the index
|
||||||
|
InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("reset"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), AllExistingFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("checkout"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), AllExistingFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
}
|
||||||
|
if (OtherThanAddedExistingFiles.Num() > 0)
|
||||||
|
{
|
||||||
|
// revert any changes in working copy (this would fails if the asset was in "Added" state, since after "reset" it is now "untracked")
|
||||||
|
// may need to try a few times due to file locks from prior operations
|
||||||
|
bool CheckoutSuccess = false;
|
||||||
|
int32 Attempts = 10;
|
||||||
|
while( Attempts-- > 0 )
|
||||||
|
{
|
||||||
|
CheckoutSuccess = GitSourceControlUtils::RunCommand(TEXT("checkout"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), OtherThanAddedExistingFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
if (CheckoutSuccess)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
FPlatformProcess::Sleep(0.1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
InCommand.bCommandSuccessful &= CheckoutSuccess;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (InCommand.bUsingGitLfsLocking)
|
||||||
|
{
|
||||||
|
// unlock files: execute the LFS command on relative filenames
|
||||||
|
// (unlock only locked files, that is, not Added files)
|
||||||
|
TArray<FString> LockedFiles;
|
||||||
|
GitSourceControlUtils::GetLockedFiles(OtherThanAddedExistingFiles, LockedFiles);
|
||||||
|
if (LockedFiles.Num() > 0)
|
||||||
|
{
|
||||||
|
const TArray<FString>& RelativeFiles = GitSourceControlUtils::RelativeFilenames(LockedFiles, InCommand.PathToGitRoot);
|
||||||
|
InCommand.bCommandSuccessful &= GitSourceControlUtils::RunLFSCommand(TEXT("unlock"), InCommand.PathToGitRoot, InCommand.PathToGitBinary, FGitSourceControlModule::GetEmptyStringArray(), RelativeFiles,
|
||||||
|
InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
if (InCommand.bCommandSuccessful)
|
||||||
|
{
|
||||||
|
for (const auto& File : LockedFiles)
|
||||||
|
{
|
||||||
|
FGitLockedFilesCache::RemoveLockedFile(File);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
TMap<FString, FGitSourceControlState> UpdatedStates;
|
||||||
|
bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, FilesToUpdate, InCommand.ResultInfo.ErrorMessages, UpdatedStates);
|
||||||
|
if (bSuccess)
|
||||||
|
{
|
||||||
|
GitSourceControlUtils::CollectNewStates(UpdatedStates, States);
|
||||||
|
}
|
||||||
|
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
|
||||||
|
|
||||||
|
return InCommand.bCommandSuccessful;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitRevertWorker::UpdateStates() const
|
||||||
|
{
|
||||||
|
return GitSourceControlUtils::UpdateCachedStates(States);
|
||||||
|
}
|
||||||
|
|
||||||
|
FName FGitSyncWorker::GetName() const
|
||||||
|
{
|
||||||
|
return "Sync";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSyncWorker::Execute(FGitSourceControlCommand& InCommand)
|
||||||
|
{
|
||||||
|
TArray<FString> Results;
|
||||||
|
const bool bFetched = GitSourceControlUtils::FetchRemote(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, false, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
if (!bFetched)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
InCommand.bCommandSuccessful = GitSourceControlUtils::PullOrigin(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.Files, InCommand.Files, Results, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
|
||||||
|
// now update the status of our files
|
||||||
|
TMap<FString, FGitSourceControlState> UpdatedStates;
|
||||||
|
const bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking,
|
||||||
|
InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates);
|
||||||
|
if (bSuccess)
|
||||||
|
{
|
||||||
|
GitSourceControlUtils::CollectNewStates(UpdatedStates, States);
|
||||||
|
}
|
||||||
|
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
|
||||||
|
GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary);
|
||||||
|
|
||||||
|
return InCommand.bCommandSuccessful;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSyncWorker::UpdateStates() const
|
||||||
|
{
|
||||||
|
return GitSourceControlUtils::UpdateCachedStates(States);
|
||||||
|
}
|
||||||
|
|
||||||
|
FName FGitFetch::GetName() const
|
||||||
|
{
|
||||||
|
return "Fetch";
|
||||||
|
}
|
||||||
|
|
||||||
|
FText FGitFetch::GetInProgressString() const
|
||||||
|
{
|
||||||
|
// TODO Configure origin
|
||||||
|
return LOCTEXT("SourceControl_Push", "Fetching from remote origin...");
|
||||||
|
}
|
||||||
|
|
||||||
|
FName FGitFetchWorker::GetName() const
|
||||||
|
{
|
||||||
|
return "Fetch";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitFetchWorker::Execute(FGitSourceControlCommand& InCommand)
|
||||||
|
{
|
||||||
|
InCommand.bCommandSuccessful = GitSourceControlUtils::FetchRemote(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking,
|
||||||
|
InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
if (!InCommand.bCommandSuccessful)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
check(InCommand.Operation->GetName() == GetName());
|
||||||
|
TSharedRef<FGitFetch, ESPMode::ThreadSafe> Operation = StaticCastSharedRef<FGitFetch>(InCommand.Operation);
|
||||||
|
|
||||||
|
if (Operation->bUpdateStatus)
|
||||||
|
{
|
||||||
|
// Now update the status of all our files
|
||||||
|
const TArray<FString> ProjectDirs {FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()),FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir()),
|
||||||
|
FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath())};
|
||||||
|
TMap<FString, FGitSourceControlState> UpdatedStates;
|
||||||
|
InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking,
|
||||||
|
ProjectDirs, InCommand.ResultInfo.ErrorMessages, UpdatedStates);
|
||||||
|
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
|
||||||
|
if (InCommand.bCommandSuccessful)
|
||||||
|
{
|
||||||
|
GitSourceControlUtils::CollectNewStates(UpdatedStates, States);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return InCommand.bCommandSuccessful;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitFetchWorker::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)
|
||||||
|
{
|
||||||
|
TMap<FString, FGitSourceControlState> UpdatedStates;
|
||||||
|
InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates);
|
||||||
|
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
|
||||||
|
if (InCommand.bCommandSuccessful)
|
||||||
|
{
|
||||||
|
GitSourceControlUtils::CollectNewStates(UpdatedStates, States);
|
||||||
|
if (Operation->ShouldUpdateHistory())
|
||||||
|
{
|
||||||
|
for (const auto& State : UpdatedStates)
|
||||||
|
{
|
||||||
|
const FString& File = State.Key;
|
||||||
|
TGitSourceControlHistory History;
|
||||||
|
|
||||||
|
if (State.Value.IsConflicted())
|
||||||
|
{
|
||||||
|
// In case of a merge conflict, we first need to get the tip of the "remote branch" (MERGE_HEAD)
|
||||||
|
GitSourceControlUtils::RunGetHistory(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, File, true,
|
||||||
|
InCommand.ResultInfo.ErrorMessages, History);
|
||||||
|
}
|
||||||
|
// Get the history of the file in the current branch
|
||||||
|
InCommand.bCommandSuccessful &= GitSourceControlUtils::RunGetHistory(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, File, false,
|
||||||
|
InCommand.ResultInfo.ErrorMessages, History);
|
||||||
|
Histories.Add(*File, History);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// no path provided: only update the status of assets in Content/ directory and also Config files
|
||||||
|
const TArray<FString> ProjectDirs {FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()), FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir()),
|
||||||
|
FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath())};
|
||||||
|
TMap<FString, FGitSourceControlState> UpdatedStates;
|
||||||
|
InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, ProjectDirs, InCommand.ResultInfo.ErrorMessages, UpdatedStates);
|
||||||
|
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
|
||||||
|
if (InCommand.bCommandSuccessful)
|
||||||
|
{
|
||||||
|
GitSourceControlUtils::CollectNewStates(UpdatedStates, 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 bool bUsingGitLfsLocking = Provider.UsesCheckout();
|
||||||
|
|
||||||
|
// TODO without LFS : Workaround a bug with the Source Control Module not updating file state after a simple "Save" with no "Checkout" (when not using File Lock)
|
||||||
|
const FDateTime Now = bUsingGitLfsLocking ? FDateTime::Now() : FDateTime::MinValue();
|
||||||
|
|
||||||
|
// 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 committed with the new asset to perform a real rename.
|
||||||
|
// => the following is to "MarkForAdd" the redirector, but it still need to be committed by selecting the whole directory and "check-in"
|
||||||
|
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), InCommand.Files, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
|
||||||
|
if (InCommand.bCommandSuccessful)
|
||||||
|
{
|
||||||
|
GitSourceControlUtils::CollectNewStates(InCommand.Files, States, EFileState::Added, ETreeState::Staged);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TMap<FString, FGitSourceControlState> UpdatedStates;
|
||||||
|
const bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates);
|
||||||
|
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
|
||||||
|
if (bSuccess)
|
||||||
|
{
|
||||||
|
GitSourceControlUtils::CollectNewStates(UpdatedStates, States);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, FGitSourceControlModule::GetEmptyStringArray(), InCommand.Files, Results, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
|
||||||
|
// now update the status of our files
|
||||||
|
TMap<FString, FGitSourceControlState> UpdatedStates;
|
||||||
|
const bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates);
|
||||||
|
GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
|
||||||
|
if (bSuccess)
|
||||||
|
{
|
||||||
|
GitSourceControlUtils::CollectNewStates(UpdatedStates, States);
|
||||||
|
}
|
||||||
|
|
||||||
|
return InCommand.bCommandSuccessful;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitResolveWorker::UpdateStates() const
|
||||||
|
{
|
||||||
|
return GitSourceControlUtils::UpdateCachedStates(States);
|
||||||
|
}
|
||||||
|
|
||||||
|
FName FGitMoveToChangelistWorker::GetName() const
|
||||||
|
{
|
||||||
|
return "MoveToChangelist";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitMoveToChangelistWorker::UpdateStates() const
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitMoveToChangelistWorker::Execute(FGitSourceControlCommand& InCommand)
|
||||||
|
{
|
||||||
|
check(InCommand.Operation->GetName() == GetName());
|
||||||
|
|
||||||
|
FGitSourceControlChangelist DestChangelist = InCommand.Changelist;
|
||||||
|
bool bResult = false;
|
||||||
|
if(DestChangelist.GetName().Equals(TEXT("Staged")))
|
||||||
|
{
|
||||||
|
bResult = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), InCommand.Files, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
}
|
||||||
|
else if(DestChangelist.GetName().Equals(TEXT("Working")))
|
||||||
|
{
|
||||||
|
TArray<FString> Parameter;
|
||||||
|
Parameter.Add(TEXT("--staged"));
|
||||||
|
bResult = GitSourceControlUtils::RunCommand(TEXT("restore"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameter, InCommand.Files, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bResult)
|
||||||
|
{
|
||||||
|
TMap<FString, FGitSourceControlState> DummyStates;
|
||||||
|
GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.InfoMessages, DummyStates);
|
||||||
|
}
|
||||||
|
return bResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
FName FGitUpdateStagingWorker::GetName() const
|
||||||
|
{
|
||||||
|
return "UpdateChangelistsStatus";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitUpdateStagingWorker::Execute(FGitSourceControlCommand& InCommand)
|
||||||
|
{
|
||||||
|
return GitSourceControlUtils::UpdateChangelistStateByCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitUpdateStagingWorker::UpdateStates() const
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
@ -0,0 +1,210 @@
|
|||||||
|
// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com)
|
||||||
|
//
|
||||||
|
// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
|
||||||
|
// or copy at http://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "IGitSourceControlWorker.h"
|
||||||
|
#include "GitSourceControlState.h"
|
||||||
|
|
||||||
|
#include "ISourceControlOperation.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal operation used to fetch from remote
|
||||||
|
*/
|
||||||
|
class FGitFetch : public ISourceControlOperation
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// ISourceControlOperation interface
|
||||||
|
virtual FName GetName() const override;
|
||||||
|
|
||||||
|
virtual FText GetInProgressString() const override;
|
||||||
|
|
||||||
|
bool bUpdateStatus = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 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;
|
||||||
|
|
||||||
|
/** Temporary states for results */
|
||||||
|
TMap<const FString, FGitState> 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;
|
||||||
|
|
||||||
|
/** Temporary states for results */
|
||||||
|
TMap<const FString, FGitState> 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;
|
||||||
|
|
||||||
|
/** Temporary states for results */
|
||||||
|
TMap<const FString, FGitState> States;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Add an untracked file to revision 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;
|
||||||
|
|
||||||
|
/** Temporary states for results */
|
||||||
|
TMap<const FString, FGitState> States;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Delete a file and remove it from revision 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;
|
||||||
|
|
||||||
|
/** Temporary states for results */
|
||||||
|
TMap<const FString, FGitState> 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;
|
||||||
|
|
||||||
|
/** Temporary states for results */
|
||||||
|
TMap<const FString, FGitState> 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;
|
||||||
|
|
||||||
|
/** Temporary states for results */
|
||||||
|
TMap<const FString, FGitState> States;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Get revision 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 */
|
||||||
|
TMap<const FString, FGitState> 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;
|
||||||
|
|
||||||
|
/** Temporary states for results */
|
||||||
|
TMap<const FString, FGitState> 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;
|
||||||
|
|
||||||
|
/** Temporary states for results */
|
||||||
|
TMap<const FString, FGitState> States;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Git push to publish branch for its configured remote */
|
||||||
|
class FGitFetchWorker : public IGitSourceControlWorker
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~FGitFetchWorker() {}
|
||||||
|
// IGitSourceControlWorker interface
|
||||||
|
virtual FName GetName() const override;
|
||||||
|
virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
|
||||||
|
virtual bool UpdateStates() const override;
|
||||||
|
|
||||||
|
/** Temporary states for results */
|
||||||
|
TMap<const FString, FGitState> States;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FGitMoveToChangelistWorker : public IGitSourceControlWorker
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~FGitMoveToChangelistWorker() {}
|
||||||
|
// IGitSourceControlWorker interface
|
||||||
|
virtual FName GetName() const override;
|
||||||
|
virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
|
||||||
|
virtual bool UpdateStates() const override;
|
||||||
|
|
||||||
|
/** Temporary states for results */
|
||||||
|
TMap<const FString, FGitState> States;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FGitUpdateStagingWorker: public IGitSourceControlWorker
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~FGitUpdateStagingWorker() {}
|
||||||
|
// IGitSourceControlWorker interface
|
||||||
|
virtual FName GetName() const override;
|
||||||
|
virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
|
||||||
|
virtual bool UpdateStates() const override;
|
||||||
|
|
||||||
|
/** Temporary states for results */
|
||||||
|
TMap<const FString, FGitState> States;
|
||||||
|
};
|
@ -0,0 +1,909 @@
|
|||||||
|
// Copyright (c) 2014-2023 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 "GitMessageLog.h"
|
||||||
|
#include "GitSourceControlState.h"
|
||||||
|
#include "Misc/Paths.h"
|
||||||
|
#include "Misc/QueuedThreadPool.h"
|
||||||
|
#include "GitSourceControlCommand.h"
|
||||||
|
#include "ISourceControlModule.h"
|
||||||
|
#include "GitSourceControlModule.h"
|
||||||
|
#include "GitSourceControlUtils.h"
|
||||||
|
#include "SGitSourceControlSettings.h"
|
||||||
|
#include "GitSourceControlRunner.h"
|
||||||
|
#include "GitSourceControlChangelistState.h"
|
||||||
|
#include "Logging/MessageLog.h"
|
||||||
|
#include "ScopedSourceControlProgress.h"
|
||||||
|
#include "SourceControlHelpers.h"
|
||||||
|
#include "SourceControlOperations.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "Async/Async.h"
|
||||||
|
#include "GenericPlatform/GenericPlatformFile.h"
|
||||||
|
#include "HAL/FileManager.h"
|
||||||
|
#include "Interfaces/IPluginManager.h"
|
||||||
|
#include "Misc/App.h"
|
||||||
|
#include "Misc/EngineVersion.h"
|
||||||
|
#include "Misc/MessageDialog.h"
|
||||||
|
#include "UObject/ObjectSaveContext.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();
|
||||||
|
}
|
||||||
|
|
||||||
|
UPackage::PackageSavedWithContextEvent.AddStatic(&GitSourceControlUtils::UpdateFileStagingOnSaved);
|
||||||
|
|
||||||
|
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
|
||||||
|
AssetRegistryModule.Get().OnAssetRenamed().AddStatic(&GitSourceControlUtils::UpdateStateOnAssetRename);
|
||||||
|
|
||||||
|
// bForceConnection: not used anymore
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlProvider::CheckGitAvailability()
|
||||||
|
{
|
||||||
|
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
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 = true;
|
||||||
|
CheckRepositoryStatus();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bGitAvailable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlProvider::UpdateSettings()
|
||||||
|
{
|
||||||
|
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
bUsingGitLfsLocking = GitSourceControl.AccessSettings().IsUsingGitLfsLocking();
|
||||||
|
LockUser = GitSourceControl.AccessSettings().GetLfsUserName();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlProvider::CheckRepositoryStatus()
|
||||||
|
{
|
||||||
|
GitSourceControlMenu.Register();
|
||||||
|
|
||||||
|
// Make sure our settings our up to date
|
||||||
|
UpdateSettings();
|
||||||
|
|
||||||
|
// Find the path to the root Git directory (if any, else uses the ProjectDir)
|
||||||
|
const FString PathToProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
|
||||||
|
PathToRepositoryRoot = PathToProjectDir;
|
||||||
|
if (!GitSourceControlUtils::FindRootDirectory(PathToProjectDir, PathToGitRoot))
|
||||||
|
{
|
||||||
|
UE_LOG(LogSourceControl, Error, TEXT("Failed to find valid Git root directory."));
|
||||||
|
bGitRepositoryFound = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PathToRepositoryRoot = PathToGitRoot;
|
||||||
|
|
||||||
|
if (!GitSourceControlUtils::CheckGitAvailability(PathToGitBinary, &GitVersion))
|
||||||
|
{
|
||||||
|
UE_LOG(LogSourceControl, Error, TEXT("Failed to find valid Git executable."));
|
||||||
|
bGitRepositoryFound = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TUniqueFunction<void()> InitFunc = [this]()
|
||||||
|
{
|
||||||
|
if (!IsInGameThread())
|
||||||
|
{
|
||||||
|
// Wait until the module interface is valid
|
||||||
|
IModuleInterface* GitModule;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
GitModule = FModuleManager::Get().GetModule("GitSourceControl");
|
||||||
|
FPlatformProcess::Sleep(0.0f);
|
||||||
|
} while (!GitModule);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user name & email (of the repository, else from the global Git config)
|
||||||
|
GitSourceControlUtils::GetUserConfig(PathToGitBinary, PathToRepositoryRoot, UserName, UserEmail);
|
||||||
|
|
||||||
|
TMap<FString, FGitSourceControlState> States;
|
||||||
|
auto ConditionalRepoInit = [this, &States]()
|
||||||
|
{
|
||||||
|
if (!GitSourceControlUtils::GetBranchName(PathToGitBinary, PathToRepositoryRoot, BranchName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
GitSourceControlUtils::GetRemoteBranchName(PathToGitBinary, PathToRepositoryRoot, RemoteBranchName);
|
||||||
|
GitSourceControlUtils::GetRemoteUrl(PathToGitBinary, PathToRepositoryRoot, RemoteUrl);
|
||||||
|
const TArray<FString> Files{TEXT("*.uasset"), TEXT("*.umap")};
|
||||||
|
TArray<FString> LockableErrorMessages;
|
||||||
|
if (!GitSourceControlUtils::CheckLFSLockable(PathToGitBinary, PathToRepositoryRoot, Files, LockableErrorMessages))
|
||||||
|
{
|
||||||
|
for (const auto &ErrorMessage : LockableErrorMessages)
|
||||||
|
{
|
||||||
|
UE_LOG(LogSourceControl, Error, TEXT("%s"), *ErrorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const TArray<FString> ProjectDirs{FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()),
|
||||||
|
FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir()),
|
||||||
|
FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath())};
|
||||||
|
TArray<FString> StatusErrorMessages;
|
||||||
|
if (!GitSourceControlUtils::RunUpdateStatus(PathToGitBinary, PathToRepositoryRoot, bUsingGitLfsLocking, ProjectDirs, StatusErrorMessages, States))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if (ConditionalRepoInit())
|
||||||
|
{
|
||||||
|
TUniqueFunction<void()> SuccessFunc = [States, this]()
|
||||||
|
{
|
||||||
|
TMap<const FString, FGitState> Results;
|
||||||
|
if (GitSourceControlUtils::CollectNewStates(States, Results))
|
||||||
|
{
|
||||||
|
GitSourceControlUtils::UpdateCachedStates(Results);
|
||||||
|
}
|
||||||
|
Runner = new FGitSourceControlRunner();
|
||||||
|
bGitRepositoryFound = true;
|
||||||
|
};
|
||||||
|
if (FApp::IsUnattended() || IsRunningCommandlet())
|
||||||
|
{
|
||||||
|
SuccessFunc();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AsyncTask(ENamedThreads::GameThread, MoveTemp(SuccessFunc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TUniqueFunction<void()> ErrorFunc = [States, this]()
|
||||||
|
{
|
||||||
|
UE_LOG(LogSourceControl, Error, TEXT("Failed to update repo on initialization."));
|
||||||
|
bGitRepositoryFound = false;
|
||||||
|
};
|
||||||
|
if (FApp::IsUnattended() || IsRunningCommandlet())
|
||||||
|
{
|
||||||
|
ErrorFunc();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AsyncTask(ENamedThreads::GameThread, MoveTemp(ErrorFunc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (FApp::IsUnattended() || IsRunningCommandlet())
|
||||||
|
{
|
||||||
|
InitFunc();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AsyncTask(ENamedThreads::AnyHiPriThreadNormalTask, MoveTemp(InitFunc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlProvider::SetLastErrors(const TArray<FText>& InErrors)
|
||||||
|
{
|
||||||
|
|
||||||
|
FScopeLock Lock(&LastErrorsCriticalSection);
|
||||||
|
LastErrors = InErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FText> FGitSourceControlProvider::GetLastErrors() const
|
||||||
|
{
|
||||||
|
FScopeLock Lock(&LastErrorsCriticalSection);
|
||||||
|
TArray<FText> Result = LastErrors;
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 FGitSourceControlProvider::GetNumLastErrors() const
|
||||||
|
{
|
||||||
|
FScopeLock Lock(&LastErrorsCriticalSection);
|
||||||
|
return LastErrors.Num();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlProvider::Close()
|
||||||
|
{
|
||||||
|
// clear the cache
|
||||||
|
StateCache.Empty();
|
||||||
|
// Remove all extensions to the "Revision Control" menu in the Editor Toolbar
|
||||||
|
GitSourceControlMenu.Unregister();
|
||||||
|
|
||||||
|
bGitAvailable = false;
|
||||||
|
bGitRepositoryFound = false;
|
||||||
|
UserName.Empty();
|
||||||
|
UserEmail.Empty();
|
||||||
|
if (Runner)
|
||||||
|
{
|
||||||
|
delete Runner;
|
||||||
|
Runner = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) );
|
||||||
|
StateCache.Add(Filename, NewState);
|
||||||
|
return NewState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedRef<FGitSourceControlChangelistState, ESPMode::ThreadSafe> FGitSourceControlProvider::GetStateInternal(const FGitSourceControlChangelist& InChangelist)
|
||||||
|
{
|
||||||
|
TSharedRef<FGitSourceControlChangelistState, ESPMode::ThreadSafe>* State = ChangelistsStateCache.Find(InChangelist);
|
||||||
|
if (State != NULL)
|
||||||
|
{
|
||||||
|
// found cached item
|
||||||
|
return (*State);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// cache an unknown state for this item
|
||||||
|
TSharedRef<FGitSourceControlChangelistState, ESPMode::ThreadSafe> NewState = MakeShared<FGitSourceControlChangelistState>(InChangelist);
|
||||||
|
ChangelistsStateCache.Add(InChangelist, NewState);
|
||||||
|
return NewState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FText FGitSourceControlProvider::GetStatusText() const
|
||||||
|
{
|
||||||
|
FFormatNamedArguments Args;
|
||||||
|
Args.Add(TEXT("IsAvailable"), (IsEnabled() && IsAvailable()) ? LOCTEXT("Yes", "Yes") : LOCTEXT("No", "No"));
|
||||||
|
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) );
|
||||||
|
|
||||||
|
FText FormattedError;
|
||||||
|
const TArray<FText>& RecentErrors = GetLastErrors();
|
||||||
|
if (RecentErrors.Num() > 0)
|
||||||
|
{
|
||||||
|
FFormatNamedArguments ErrorArgs;
|
||||||
|
ErrorArgs.Add(TEXT("ErrorText"), RecentErrors[0]);
|
||||||
|
|
||||||
|
FormattedError = FText::Format(LOCTEXT("GitErrorStatusText", "Error: {ErrorText}\n\n"), ErrorArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
Args.Add(TEXT("ErrorText"), FormattedError);
|
||||||
|
|
||||||
|
return FText::Format( NSLOCTEXT("GitStatusText", "{ErrorText}Enabled: {IsAvailable}", "Local repository: {RepositoryName}\nRemote: {RemoteUrl}\nUser: {UserName}\nE-mail: {UserEmail}\n[{BranchName} {CommitId}] {CommitSummary}"), Args );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Quick check if revision control is enabled */
|
||||||
|
bool FGitSourceControlProvider::IsEnabled() const
|
||||||
|
{
|
||||||
|
return bGitRepositoryFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Quick check if revision 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (InStateCacheUsage == EStateCacheUsage::ForceUpdate)
|
||||||
|
{
|
||||||
|
TArray<FString> ForceUpdate;
|
||||||
|
for (FString Path : InFiles)
|
||||||
|
{
|
||||||
|
// Remove the path from the cache, so it's not ignored the next time we force check.
|
||||||
|
// If the file isn't in the cache, force update it now.
|
||||||
|
if (!RemoveFileFromIgnoreForceCache(Path))
|
||||||
|
{
|
||||||
|
ForceUpdate.Add(Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ForceUpdate.Num() > 0)
|
||||||
|
{
|
||||||
|
Execute(ISourceControlOperation::Create<FUpdateStatus>(), ForceUpdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TArray<FString>& AbsoluteFiles = SourceControlHelpers::AbsoluteFilenames(InFiles);
|
||||||
|
|
||||||
|
for (TArray<FString>::TConstIterator It(AbsoluteFiles); It; It++)
|
||||||
|
{
|
||||||
|
OutState.Add(GetStateInternal(*It));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ECommandResult::Succeeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
ECommandResult::Type FGitSourceControlProvider::GetState(const TArray<FSourceControlChangelistRef>& InChangelists, TArray<FSourceControlChangelistStateRef>& OutState, EStateCacheUsage::Type InStateCacheUsage)
|
||||||
|
{
|
||||||
|
if (!IsEnabled())
|
||||||
|
{
|
||||||
|
return ECommandResult::Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (FSourceControlChangelistRef Changelist : InChangelists)
|
||||||
|
{
|
||||||
|
FGitSourceControlChangelistRef GitChangelist = StaticCastSharedRef<FGitSourceControlChangelist>(Changelist);
|
||||||
|
OutState.Add(GetStateInternal(GitChangelist.Get()));
|
||||||
|
}
|
||||||
|
return ECommandResult::Succeeded;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
TArray<FSourceControlStateRef> FGitSourceControlProvider::GetCachedStateByPredicate(TFunctionRef<bool(const FSourceControlStateRef&)> Predicate) const
|
||||||
|
{
|
||||||
|
TArray<FSourceControlStateRef> Result;
|
||||||
|
for (const auto& CacheItem : StateCache)
|
||||||
|
{
|
||||||
|
const FSourceControlStateRef& State = CacheItem.Value;
|
||||||
|
if (Predicate(State))
|
||||||
|
{
|
||||||
|
Result.Add(State);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlProvider::RemoveFileFromCache(const FString& Filename)
|
||||||
|
{
|
||||||
|
return StateCache.Remove(Filename) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlProvider::AddFileToIgnoreForceCache(const FString& Filename)
|
||||||
|
{
|
||||||
|
return IgnoreForceCache.Add(Filename) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlProvider::RemoveFileFromIgnoreForceCache(const FString& Filename)
|
||||||
|
{
|
||||||
|
return IgnoreForceCache.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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION < 5
|
||||||
|
ECommandResult::Type FGitSourceControlProvider::Execute( const FSourceControlOperationRef& InOperation, const TArray<FString>& InFiles, EConcurrency::Type InConcurrency, const FSourceControlOperationComplete& InOperationCompleteDelegate )
|
||||||
|
#else
|
||||||
|
ECommandResult::Type FGitSourceControlProvider::Execute( const FSourceControlOperationRef& InOperation, FSourceControlChangelistPtr InChangelist, const TArray<FString>& InFiles, EConcurrency::Type InConcurrency, const FSourceControlOperationComplete& InOperationCompleteDelegate )
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
if(!IsEnabled() && !(InOperation->GetName() == "Connect")) // Only Connect operation allowed while not Enabled (Repository found)
|
||||||
|
{
|
||||||
|
InOperationCompleteDelegate.ExecuteIfBound(InOperation, ECommandResult::Failed);
|
||||||
|
return ECommandResult::Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const 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 revision 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 revision control provider '{ProviderName}'"), Arguments));
|
||||||
|
|
||||||
|
FTSMessageLog("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->UpdateRepositoryRootIfSubmodule(AbsoluteFiles);
|
||||||
|
Command->OperationCompleteDelegate = InOperationCompleteDelegate;
|
||||||
|
|
||||||
|
TSharedPtr<FGitSourceControlChangelist, ESPMode::ThreadSafe> ChangelistPtr = StaticCastSharedPtr<FGitSourceControlChangelist>(InChangelist);
|
||||||
|
Command->Changelist = ChangelistPtr ? ChangelistPtr.ToSharedRef().Get() : FGitSourceControlChangelist();
|
||||||
|
|
||||||
|
// fire off operation
|
||||||
|
if(InConcurrency == EConcurrency::Synchronous)
|
||||||
|
{
|
||||||
|
Command->bAutoDelete = false;
|
||||||
|
|
||||||
|
#if UE_BUILD_DEBUG
|
||||||
|
UE_LOG(LogSourceControl, Log, TEXT("ExecuteSynchronousCommand(%s)"), *InOperation->GetName().ToString());
|
||||||
|
#endif
|
||||||
|
return ExecuteSynchronousCommand(*Command, InOperation->GetInProgressString(), false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Command->bAutoDelete = true;
|
||||||
|
|
||||||
|
#if UE_BUILD_DEBUG
|
||||||
|
UE_LOG(LogSourceControl, Log, TEXT("IssueAsynchronousCommand(%s)"), *InOperation->GetName().ToString());
|
||||||
|
#endif
|
||||||
|
return IssueCommand(*Command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION < 5
|
||||||
|
bool FGitSourceControlProvider::CanCancelOperation( const FSourceControlOperationRef& InOperation ) const
|
||||||
|
#else
|
||||||
|
bool FGitSourceControlProvider::CanCancelOperation( const FSourceControlOperationRef& InOperation ) const
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
// TODO: maybe support cancellation again?
|
||||||
|
#if 0
|
||||||
|
for (int32 CommandIndex = 0; CommandIndex < CommandQueue.Num(); ++CommandIndex)
|
||||||
|
{
|
||||||
|
const FGitSourceControlCommand& Command = *CommandQueue[CommandIndex];
|
||||||
|
if (Command.Operation == InOperation)
|
||||||
|
{
|
||||||
|
check(Command.bAutoDelete);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// operation was not in progress!
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION < 5
|
||||||
|
void FGitSourceControlProvider::CancelOperation( const FSourceControlOperationRef& InOperation )
|
||||||
|
#else
|
||||||
|
void FGitSourceControlProvider::CancelOperation( const FSourceControlOperationRef& InOperation )
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
for (int32 CommandIndex = 0; CommandIndex < CommandQueue.Num(); ++CommandIndex)
|
||||||
|
{
|
||||||
|
FGitSourceControlCommand& Command = *CommandQueue[CommandIndex];
|
||||||
|
if (Command.Operation == InOperation)
|
||||||
|
{
|
||||||
|
check(Command.bAutoDelete);
|
||||||
|
Command.Cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlProvider::UsesLocalReadOnlyState() const
|
||||||
|
{
|
||||||
|
return bUsingGitLfsLocking; // Git LFS Lock uses read-only state
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlProvider::UsesChangelists() const
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlProvider::UsesCheckout() const
|
||||||
|
{
|
||||||
|
return bUsingGitLfsLocking; // Git LFS Lock uses read-only state
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
|
||||||
|
bool FGitSourceControlProvider::UsesFileRevisions() const
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TOptional<bool> FGitSourceControlProvider::IsAtLatestRevision() const
|
||||||
|
{
|
||||||
|
return TOptional<bool>();
|
||||||
|
}
|
||||||
|
|
||||||
|
TOptional<int> FGitSourceControlProvider::GetNumLocalChanges() const
|
||||||
|
{
|
||||||
|
return TOptional<int>();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2
|
||||||
|
bool FGitSourceControlProvider::AllowsDiffAgainstDepot() const
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlProvider::UsesUncontrolledChangelists() const
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlProvider::UsesSnapshots() const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
|
||||||
|
bool FGitSourceControlProvider::CanExecuteOperation(const FSourceControlOperationRef& InOperation) const {
|
||||||
|
return WorkersMap.Find(InOperation->GetName()) != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
TMap<ISourceControlProvider::EStatus, FString> FGitSourceControlProvider::GetStatus() const
|
||||||
|
{
|
||||||
|
TMap<EStatus, FString> Result;
|
||||||
|
Result.Add(EStatus::Enabled, IsEnabled() ? TEXT("Yes") : TEXT("No") );
|
||||||
|
Result.Add(EStatus::Connected, (IsEnabled() && IsAvailable()) ? TEXT("Yes") : TEXT("No") );
|
||||||
|
Result.Add(EStatus::User, UserName);
|
||||||
|
Result.Add(EStatus::Repository, PathToRepositoryRoot);
|
||||||
|
Result.Add(EStatus::Remote, RemoteUrl);
|
||||||
|
Result.Add(EStatus::Branch, BranchName);
|
||||||
|
Result.Add(EStatus::Email, UserEmail);
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
FTSMessageLog SourceControlLog("SourceControl");
|
||||||
|
|
||||||
|
for (int32 ErrorIndex = 0; ErrorIndex < InCommand.ResultInfo.ErrorMessages.Num(); ++ErrorIndex)
|
||||||
|
{
|
||||||
|
SourceControlLog.Error(FText::FromString(InCommand.ResultInfo.ErrorMessages[ErrorIndex]));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32 InfoIndex = 0; InfoIndex < InCommand.ResultInfo.InfoMessages.Num(); ++InfoIndex)
|
||||||
|
{
|
||||||
|
SourceControlLog.Info(FText::FromString(InCommand.ResultInfo.InfoMessages[InfoIndex]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlProvider::UpdateRepositoryStatus(const class FGitSourceControlCommand& InCommand)
|
||||||
|
{
|
||||||
|
// For all operations running UpdateStatus, get Commit information:
|
||||||
|
if (!InCommand.CommitId.IsEmpty())
|
||||||
|
{
|
||||||
|
CommitId = InCommand.CommitId;
|
||||||
|
CommitSummary = InCommand.CommitSummary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlProvider::Tick()
|
||||||
|
{
|
||||||
|
#if ENGINE_MAJOR_VERSION < 5
|
||||||
|
bool bStatesUpdated = false;
|
||||||
|
#else
|
||||||
|
bool bStatesUpdated = TicksUntilNextForcedUpdate == 1;
|
||||||
|
if( TicksUntilNextForcedUpdate > 0 )
|
||||||
|
{
|
||||||
|
--TicksUntilNextForcedUpdate;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
for (int32 CommandIndex = 0; CommandIndex < CommandQueue.Num(); ++CommandIndex)
|
||||||
|
{
|
||||||
|
FGitSourceControlCommand& Command = *CommandQueue[CommandIndex];
|
||||||
|
|
||||||
|
if (Command.bExecuteProcessed)
|
||||||
|
{
|
||||||
|
// Remove command from the queue
|
||||||
|
CommandQueue.RemoveAt(CommandIndex);
|
||||||
|
|
||||||
|
if (!Command.IsCanceled())
|
||||||
|
{
|
||||||
|
// Update repository 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
|
||||||
|
if (!Command.IsCanceled())
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
else if (Command.bCancelled)
|
||||||
|
{
|
||||||
|
// If this was a synchronous command, set it free so that it will be deleted automatically
|
||||||
|
// when its (still running) thread finally finishes
|
||||||
|
Command.bAutoDelete = true;
|
||||||
|
|
||||||
|
Command.ReturnResults();
|
||||||
|
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 ENGINE_MAJOR_VERSION >= 5
|
||||||
|
TArray<FSourceControlChangelistRef> FGitSourceControlProvider::GetChangelists( EStateCacheUsage::Type InStateCacheUsage )
|
||||||
|
{
|
||||||
|
if (!IsEnabled())
|
||||||
|
{
|
||||||
|
return TArray<FSourceControlChangelistRef>();
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FSourceControlChangelistRef> Changelists;
|
||||||
|
Algo::Transform(ChangelistsStateCache, Changelists, [](const auto& Pair) { return MakeShared<FGitSourceControlChangelist, ESPMode::ThreadSafe>(Pair.Key); });
|
||||||
|
return Changelists;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if SOURCE_CONTROL_WITH_SLATE
|
||||||
|
TSharedRef<class SWidget> FGitSourceControlProvider::MakeSettingsWidget() const
|
||||||
|
{
|
||||||
|
return SNew(SGitSourceControlSettings);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
ECommandResult::Type FGitSourceControlProvider::ExecuteSynchronousCommand(FGitSourceControlCommand& InCommand, const FText& Task, bool bSuppressResponseMsg)
|
||||||
|
{
|
||||||
|
ECommandResult::Type Result = ECommandResult::Failed;
|
||||||
|
|
||||||
|
struct Local
|
||||||
|
{
|
||||||
|
static void CancelCommand(FGitSourceControlCommand* InControlCommand)
|
||||||
|
{
|
||||||
|
InControlCommand->Cancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
FText TaskText = Task;
|
||||||
|
// Display the progress dialog
|
||||||
|
if (bSuppressResponseMsg)
|
||||||
|
{
|
||||||
|
TaskText = FText::GetEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
// Display the progress dialog if a string was provided
|
||||||
|
{
|
||||||
|
// TODO: support cancellation?
|
||||||
|
//FScopedSourceControlProgress Progress(TaskText, FSimpleDelegate::CreateStatic(&Local::CancelCommand, &InCommand));
|
||||||
|
FScopedSourceControlProgress Progress(TaskText);
|
||||||
|
|
||||||
|
// Issue the command asynchronously...
|
||||||
|
IssueCommand( InCommand );
|
||||||
|
|
||||||
|
// ... then wait for its completion (thus making it synchronous)
|
||||||
|
while (!InCommand.IsCanceled() && CommandQueue.Contains(&InCommand))
|
||||||
|
{
|
||||||
|
// Tick the command queue and update progress.
|
||||||
|
Tick();
|
||||||
|
|
||||||
|
if (i >= 20) {
|
||||||
|
Progress.Tick();
|
||||||
|
i = 0;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
|
||||||
|
// Sleep for a bit so we don't busy-wait so much.
|
||||||
|
FPlatformProcess::Sleep(0.01f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (InCommand.bCancelled)
|
||||||
|
{
|
||||||
|
Result = ECommandResult::Cancelled;
|
||||||
|
}
|
||||||
|
if (InCommand.bCommandSuccessful)
|
||||||
|
{
|
||||||
|
Result = ECommandResult::Succeeded;
|
||||||
|
}
|
||||||
|
else if (!bSuppressResponseMsg)
|
||||||
|
{
|
||||||
|
FMessageDialog::Open( EAppMsgType::Ok, LOCTEXT("Git_ServerUnresponsive", "Git command failed. Please check your connection and try again, or check the output log for more information.") );
|
||||||
|
UE_LOG(LogSourceControl, Error, TEXT("Command '%s' Failed!"), *InCommand.Operation->GetName().ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the command now if not marked as auto-delete
|
||||||
|
if (!InCommand.bAutoDelete)
|
||||||
|
{
|
||||||
|
delete &InCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
ECommandResult::Type FGitSourceControlProvider::IssueCommand(FGitSourceControlCommand& InCommand, const bool bSynchronous)
|
||||||
|
{
|
||||||
|
if (!bSynchronous && GThreadPool != nullptr)
|
||||||
|
{
|
||||||
|
// Queue this to our worker thread(s) for resolving.
|
||||||
|
// When asynchronous, any callback gets called from Tick().
|
||||||
|
GThreadPool->AddQueuedWork(&InCommand);
|
||||||
|
CommandQueue.Add(&InCommand);
|
||||||
|
return ECommandResult::Succeeded;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UE_LOG(LogSourceControl, Log, TEXT("There are no threads available to process the revision control command '%s'. Running synchronously."), *InCommand.Operation->GetName().ToString());
|
||||||
|
|
||||||
|
InCommand.bCommandSuccessful = InCommand.DoWork();
|
||||||
|
|
||||||
|
InCommand.Worker->UpdateStates();
|
||||||
|
|
||||||
|
OutputCommandMessages(InCommand);
|
||||||
|
|
||||||
|
// Callback now if present. When asynchronous, this callback gets called from Tick().
|
||||||
|
return InCommand.ReturnResults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlProvider::QueryStateBranchConfig(const FString& ConfigSrc, const FString& ConfigDest)
|
||||||
|
{
|
||||||
|
// Check similar preconditions to Perforce (valid src and dest),
|
||||||
|
if (ConfigSrc.Len() == 0 || ConfigDest.Len() == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bGitAvailable || !bGitRepositoryFound)
|
||||||
|
{
|
||||||
|
FTSMessageLog("SourceControl").Error(LOCTEXT("StatusBranchConfigNoConnection", "Unable to retrieve status branch configuration from repo, no connection"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we can assume that whatever our user is doing to config state branches is properly synced, so just copy.
|
||||||
|
// TODO: maybe don't assume, and use git show instead?
|
||||||
|
IFileManager::Get().Copy(*ConfigDest, *ConfigSrc);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlProvider::RegisterStateBranches(const TArray<FString>& BranchNames, const FString& ContentRootIn)
|
||||||
|
{
|
||||||
|
StatusBranchNamePatternsInternal = BranchNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 FGitSourceControlProvider::GetStateBranchIndex(const FString& StateBranchName) const
|
||||||
|
{
|
||||||
|
// How do state branches indices work?
|
||||||
|
// Order matters. Lower values are lower in the hierarchy, i.e., changes from higher branches get automatically merged down.
|
||||||
|
// The higher branch is, the stabler it is, and has changes manually promoted up.
|
||||||
|
|
||||||
|
// Check if we are checking the index of the current branch
|
||||||
|
// UE uses FEngineVersion for the current branch name because of UEGames setup, but we want to handle otherwise for Git repos.
|
||||||
|
auto StatusBranchNames = GetStatusBranchNames();
|
||||||
|
if (StateBranchName == FEngineVersion::Current().GetBranch())
|
||||||
|
{
|
||||||
|
const int32 CurrentBranchStatusIndex = StatusBranchNames.IndexOfByKey(BranchName);
|
||||||
|
const bool bCurrentBranchInStatusBranches = CurrentBranchStatusIndex != INDEX_NONE;
|
||||||
|
// If the user's current branch is tracked as a status branch, give the proper index
|
||||||
|
if (bCurrentBranchInStatusBranches)
|
||||||
|
{
|
||||||
|
return CurrentBranchStatusIndex;
|
||||||
|
}
|
||||||
|
// If the current branch is not a status branch, make it the highest branch
|
||||||
|
// This is semantically correct, since if a branch is not marked as a status branch
|
||||||
|
// it merges changes in a similar fashion to the highest status branch, i.e. manually promotes them
|
||||||
|
// based on the user merging those changes in. and these changes always get merged from even the highest point
|
||||||
|
// of the stream. i.e, promoted/stable changes are always up for consumption by this branch.
|
||||||
|
return INT32_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're not checking the current branch, then we don't need to do special handling.
|
||||||
|
// If it is not a status branch, there is no message
|
||||||
|
return StatusBranchNames.IndexOfByKey(StateBranchName);
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FString> FGitSourceControlProvider::GetStatusBranchNames() const
|
||||||
|
{
|
||||||
|
TArray<FString> StatusBranches;
|
||||||
|
if(PathToGitBinary.IsEmpty() || PathToRepositoryRoot.IsEmpty())
|
||||||
|
return StatusBranches;
|
||||||
|
|
||||||
|
for (int i = 0; i < StatusBranchNamePatternsInternal.Num(); i++)
|
||||||
|
{
|
||||||
|
TArray<FString> Matches;
|
||||||
|
bool bResult = GitSourceControlUtils::GetRemoteBranchesWildcard(PathToGitBinary, PathToRepositoryRoot, StatusBranchNamePatternsInternal[i], Matches);
|
||||||
|
if (bResult && Matches.Num() > 0)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < Matches.Num(); j++)
|
||||||
|
{
|
||||||
|
StatusBranches.Add(Matches[j].TrimStartAndEnd());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatusBranches;
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
@ -0,0 +1,308 @@
|
|||||||
|
// 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 "GitSourceControlChangelist.h"
|
||||||
|
#include "ISourceControlProvider.h"
|
||||||
|
#include "IGitSourceControlWorker.h"
|
||||||
|
#include "GitSourceControlMenu.h"
|
||||||
|
#include "Runtime/Launch/Resources/Version.h"
|
||||||
|
|
||||||
|
class FGitSourceControlChangelistState;
|
||||||
|
class FGitSourceControlState;
|
||||||
|
|
||||||
|
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), "git version 2.11.0" (Linux/Mac/Cygwin/WSL) or "git version 2.31.1.vfs.0.3" (Microsoft)
|
||||||
|
int Major; // 2 Major version number
|
||||||
|
int Minor; // 31 Minor version number
|
||||||
|
int Patch; // 1 Patch/bugfix number
|
||||||
|
bool bIsFork;
|
||||||
|
FString Fork; // "vfs"
|
||||||
|
int ForkMajor; // 0 Fork specific revision number
|
||||||
|
int ForkMinor; // 3
|
||||||
|
int ForkPatch; // ?
|
||||||
|
|
||||||
|
FGitVersion()
|
||||||
|
: Major(0)
|
||||||
|
, Minor(0)
|
||||||
|
, Patch(0)
|
||||||
|
, bIsFork(false)
|
||||||
|
, ForkMajor(0)
|
||||||
|
, ForkMinor(0)
|
||||||
|
, ForkPatch(0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class FGitSourceControlProvider : public ISourceControlProvider
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/* 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;
|
||||||
|
virtual void RegisterStateBranches(const TArray<FString>& BranchNames, const FString& ContentRootIn) override;
|
||||||
|
virtual int32 GetStateBranchIndex(const FString& BranchName) const override;
|
||||||
|
virtual ECommandResult::Type GetState( const TArray<FString>& InFiles, TArray<FSourceControlStateRef>& OutState, EStateCacheUsage::Type InStateCacheUsage ) override;
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
virtual ECommandResult::Type GetState(const TArray<FSourceControlChangelistRef>& InChangelists, TArray<FSourceControlChangelistStateRef>& OutState, EStateCacheUsage::Type InStateCacheUsage) override;
|
||||||
|
#endif
|
||||||
|
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;
|
||||||
|
#if ENGINE_MAJOR_VERSION < 5
|
||||||
|
virtual ECommandResult::Type Execute( const FSourceControlOperationRef& InOperation, const TArray<FString>& InFiles, EConcurrency::Type InConcurrency = EConcurrency::Synchronous, const FSourceControlOperationComplete& InOperationCompleteDelegate = FSourceControlOperationComplete()) override;
|
||||||
|
virtual bool CanCancelOperation( const FSourceControlOperationRef& InOperation ) const override;
|
||||||
|
virtual void CancelOperation( const FSourceControlOperationRef& InOperation ) override;
|
||||||
|
#else
|
||||||
|
virtual ECommandResult::Type Execute(const FSourceControlOperationRef& InOperation, FSourceControlChangelistPtr InChangelist, const TArray<FString>& InFiles, EConcurrency::Type InConcurrency = EConcurrency::Synchronous, const FSourceControlOperationComplete& InOperationCompleteDelegate = FSourceControlOperationComplete()) override;
|
||||||
|
virtual bool CanCancelOperation( const FSourceControlOperationRef& InOperation ) const override;
|
||||||
|
virtual void CancelOperation( const FSourceControlOperationRef& InOperation ) override;
|
||||||
|
#endif
|
||||||
|
virtual bool UsesLocalReadOnlyState() const override;
|
||||||
|
virtual bool UsesChangelists() const override;
|
||||||
|
virtual bool UsesCheckout() const override;
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
|
||||||
|
virtual bool UsesFileRevisions() const override;
|
||||||
|
virtual TOptional<bool> IsAtLatestRevision() const override;
|
||||||
|
virtual TOptional<int> GetNumLocalChanges() const override;
|
||||||
|
#endif
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2
|
||||||
|
virtual bool AllowsDiffAgainstDepot() const override;
|
||||||
|
virtual bool UsesUncontrolledChangelists() const override;
|
||||||
|
virtual bool UsesSnapshots() const override;
|
||||||
|
#endif
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
|
||||||
|
virtual bool CanExecuteOperation( const FSourceControlOperationRef& InOperation ) const override;
|
||||||
|
virtual TMap<EStatus, FString> GetStatus() const override;
|
||||||
|
#endif
|
||||||
|
virtual void Tick() override;
|
||||||
|
virtual TArray< TSharedRef<class ISourceControlLabel> > GetLabels( const FString& InMatchingSpec ) const override;
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
virtual TArray<FSourceControlChangelistRef> GetChangelists( EStateCacheUsage::Type InStateCacheUsage ) override;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if SOURCE_CONTROL_WITH_SLATE
|
||||||
|
virtual TSharedRef<class SWidget> MakeSettingsWidget() const override;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using ISourceControlProvider::Execute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check configuration, else standard paths, and run a Git "version" command to check the availability of the binary.
|
||||||
|
*/
|
||||||
|
void CheckGitAvailability();
|
||||||
|
|
||||||
|
/** Refresh Git settings from revision control settings */
|
||||||
|
void UpdateSettings();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the .git/ repository and check its status.
|
||||||
|
*/
|
||||||
|
void CheckRepositoryStatus();
|
||||||
|
|
||||||
|
/** Is git binary found and working. */
|
||||||
|
inline bool IsGitAvailable() const
|
||||||
|
{
|
||||||
|
return bGitAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Git version for feature checking */
|
||||||
|
inline const FGitVersion& GetGitVersion() const
|
||||||
|
{
|
||||||
|
return GitVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Path to the root of the Unreal revision control repository: usually the ProjectDir */
|
||||||
|
inline const FString& GetPathToRepositoryRoot() const
|
||||||
|
{
|
||||||
|
return PathToRepositoryRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Path to the root of the Git repository: can be the ProjectDir itself, or any parent directory (found by the "Connect" operation) */
|
||||||
|
inline const FString& GetPathToGitRoot() const
|
||||||
|
{
|
||||||
|
return PathToGitRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the path to the Git binary */
|
||||||
|
inline const FString& GetGitBinaryPath() const
|
||||||
|
{
|
||||||
|
return PathToGitBinary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const FString& GetLockUser() const
|
||||||
|
{
|
||||||
|
return LockUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helper function used to update state cache */
|
||||||
|
TSharedRef<FGitSourceControlState, ESPMode::ThreadSafe> GetStateInternal(const FString& Filename);
|
||||||
|
|
||||||
|
/** Helper function used to update changelists state cache */
|
||||||
|
TSharedRef<FGitSourceControlChangelistState, ESPMode::ThreadSafe> GetStateInternal(const FGitSourceControlChangelist& InChangelist);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 );
|
||||||
|
|
||||||
|
/** Set list of error messages that occurred after last perforce command */
|
||||||
|
void SetLastErrors(const TArray<FText>& InErrors);
|
||||||
|
|
||||||
|
/** Get list of error messages that occurred after last perforce command */
|
||||||
|
TArray<FText> GetLastErrors() const;
|
||||||
|
|
||||||
|
/** Get number of error messages seen after running last perforce command */
|
||||||
|
int32 GetNumLastErrors() const;
|
||||||
|
|
||||||
|
/** Remove a named file from the state cache */
|
||||||
|
bool RemoveFileFromCache(const FString& Filename);
|
||||||
|
|
||||||
|
/** Get files in cache */
|
||||||
|
TArray<FString> GetFilesInCache();
|
||||||
|
|
||||||
|
bool AddFileToIgnoreForceCache(const FString& Filename);
|
||||||
|
|
||||||
|
bool RemoveFileFromIgnoreForceCache(const FString& Filename);
|
||||||
|
|
||||||
|
const FString& GetBranchName() const
|
||||||
|
{
|
||||||
|
return BranchName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString& GetRemoteBranchName() const { return RemoteBranchName; }
|
||||||
|
|
||||||
|
TArray<FString> GetStatusBranchNames() const;
|
||||||
|
|
||||||
|
/** Indicates editor binaries are to be updated upon next sync */
|
||||||
|
bool bPendingRestart;
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
uint32 TicksUntilNextForcedUpdate = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private:
|
||||||
|
/** Is git binary found and working. */
|
||||||
|
bool bGitAvailable = false;
|
||||||
|
|
||||||
|
/** Is git repository found. */
|
||||||
|
bool bGitRepositoryFound = false;
|
||||||
|
|
||||||
|
/** Is LFS locking enabled? */
|
||||||
|
bool bUsingGitLfsLocking = false;
|
||||||
|
|
||||||
|
FString PathToGitBinary;
|
||||||
|
|
||||||
|
FString LockUser;
|
||||||
|
|
||||||
|
/** Critical section for thread safety of error messages that occurred after last perforce command */
|
||||||
|
mutable FCriticalSection LastErrorsCriticalSection;
|
||||||
|
|
||||||
|
/** List of error messages that occurred after last perforce command */
|
||||||
|
TArray<FText> LastErrors;
|
||||||
|
|
||||||
|
/** 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, bool bSuppressResponseMsg);
|
||||||
|
/** Issue a command asynchronously if possible. */
|
||||||
|
ECommandResult::Type IssueCommand(class FGitSourceControlCommand& InCommand, const bool bSynchronous = false );
|
||||||
|
|
||||||
|
/** 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 Unreal revision control repository: usually the ProjectDir */
|
||||||
|
FString PathToRepositoryRoot;
|
||||||
|
|
||||||
|
/** Path to the root of the Git repository: can be the ProjectDir itself, or any parent directory (found by the "Connect" operation) */
|
||||||
|
FString PathToGitRoot;
|
||||||
|
|
||||||
|
/** 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;
|
||||||
|
|
||||||
|
/** Name of the current remote branch */
|
||||||
|
FString RemoteBranchName;
|
||||||
|
|
||||||
|
/** URL of the "origin" default 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;
|
||||||
|
TMap<FGitSourceControlChangelist, TSharedRef<class FGitSourceControlChangelistState, ESPMode::ThreadSafe> > ChangelistsStateCache;
|
||||||
|
|
||||||
|
/** The currently registered revision control operations */
|
||||||
|
TMap<FName, FGetGitSourceControlWorker> WorkersMap;
|
||||||
|
|
||||||
|
/** Queue for commands given by the main thread */
|
||||||
|
TArray < FGitSourceControlCommand* > CommandQueue;
|
||||||
|
|
||||||
|
/** For notifying when the revision control states in the cache have changed */
|
||||||
|
FSourceControlStateChanged OnSourceControlStateChanged;
|
||||||
|
|
||||||
|
/** Git version for feature checking */
|
||||||
|
FGitVersion GitVersion;
|
||||||
|
|
||||||
|
/** Revision Control Menu Extension */
|
||||||
|
FGitSourceControlMenu GitSourceControlMenu;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Ignore these files when forcing status updates. We add to this list when we've just updated the status already.
|
||||||
|
UE's SourceControl has a habit of performing a double status update, immediately after an operation.
|
||||||
|
*/
|
||||||
|
TArray<FString> IgnoreForceCache;
|
||||||
|
|
||||||
|
/** Array of branch name patterns for status queries */
|
||||||
|
TArray<FString> StatusBranchNamePatternsInternal;
|
||||||
|
|
||||||
|
class FGitSourceControlRunner* Runner = nullptr;
|
||||||
|
};
|
@ -0,0 +1,134 @@
|
|||||||
|
// Copyright (c) 2014-2023 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"
|
||||||
|
#include "ISourceControlModule.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "GitSourceControl"
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
bool FGitSourceControlRevision::Get( FString& InOutFilename, EConcurrency::Type InConcurrency ) const
|
||||||
|
{
|
||||||
|
if (InConcurrency != EConcurrency::Synchronous)
|
||||||
|
{
|
||||||
|
UE_LOG(LogSourceControl, Warning, TEXT("Only EConcurrency::Synchronous is tested/supported for this operation."));
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
bool FGitSourceControlRevision::Get( FString& InOutFilename ) const
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
const FGitSourceControlModule* GitSourceControl = FGitSourceControlModule::GetThreadSafe();
|
||||||
|
if (!GitSourceControl)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const FGitSourceControlProvider& Provider = GitSourceControl->GetProvider();
|
||||||
|
const FString PathToGitBinary = Provider.GetGitBinaryPath();
|
||||||
|
FString PathToRepositoryRoot = Provider.GetPathToRepositoryRoot();
|
||||||
|
// the repo root can be customised if in a plugin that has it's own repo
|
||||||
|
if (PathToRepoRoot.Len())
|
||||||
|
{
|
||||||
|
PathToRepositoryRoot = PathToRepoRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if a filename for the temp file wasn't supplied generate a unique-ish one
|
||||||
|
if(InOutFilename.Len() == 0)
|
||||||
|
{
|
||||||
|
// create the diff dir if we don't already have it (Git wont)
|
||||||
|
IFileManager::Get().MakeDirectory(*FPaths::DiffDir(), true);
|
||||||
|
// create a unique temp file name based on the unique commit Id
|
||||||
|
const FString TempFileName = FString::Printf(TEXT("%stemp-%s-%s"), *FPaths::DiffDir(), *CommitId, *FPaths::GetCleanFilename(Filename));
|
||||||
|
InOutFilename = FPaths::ConvertRelativePathToFull(TempFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diff against the revision
|
||||||
|
const FString Parameter = FString::Printf(TEXT("%s:%s"), *CommitId, *Filename);
|
||||||
|
|
||||||
|
bool bCommandSuccessful;
|
||||||
|
if(FPaths::FileExists(InOutFilename))
|
||||||
|
{
|
||||||
|
bCommandSuccessful = true; // if the temp file already exists, reuse it directly
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bCommandSuccessful = GitSourceControlUtils::RunDumpToFile(PathToGitBinary, PathToRepositoryRoot, Parameter, InOutFilename);
|
||||||
|
}
|
||||||
|
return bCommandSuccessful;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlRevision::GetAnnotated( TArray<FAnnotationLine>& OutLines ) const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlRevision::GetAnnotated( FString& InOutFilename ) const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString& FGitSourceControlRevision::GetFilename() const
|
||||||
|
{
|
||||||
|
return Filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 FGitSourceControlRevision::GetRevisionNumber() const
|
||||||
|
{
|
||||||
|
return RevisionNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString& FGitSourceControlRevision::GetRevision() const
|
||||||
|
{
|
||||||
|
return ShortCommitId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString& FGitSourceControlRevision::GetDescription() const
|
||||||
|
{
|
||||||
|
return Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString& FGitSourceControlRevision::GetUserName() const
|
||||||
|
{
|
||||||
|
return UserName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString& FGitSourceControlRevision::GetClientSpec() const
|
||||||
|
{
|
||||||
|
static FString EmptyString(TEXT(""));
|
||||||
|
return EmptyString;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString& FGitSourceControlRevision::GetAction() const
|
||||||
|
{
|
||||||
|
return Action;
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FGitSourceControlRevision::GetBranchSource() const
|
||||||
|
{
|
||||||
|
// if this revision was copied/moved from some other revision
|
||||||
|
return BranchSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FDateTime& FGitSourceControlRevision::GetDate() const
|
||||||
|
{
|
||||||
|
return Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 FGitSourceControlRevision::GetCheckInIdentifier() const
|
||||||
|
{
|
||||||
|
return CommitIdNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 FGitSourceControlRevision::GetFileSize() const
|
||||||
|
{
|
||||||
|
return FileSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
@ -0,0 +1,80 @@
|
|||||||
|
// Copyright (c) 2014-2023 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 "ISourceControlRevision.h"
|
||||||
|
#include "Runtime/Launch/Resources/Version.h"
|
||||||
|
#include "Misc/DateTime.h"
|
||||||
|
|
||||||
|
/** Revision of a file, linked to a specific commit */
|
||||||
|
class FGitSourceControlRevision : public ISourceControlRevision
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/** ISourceControlRevision interface */
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
virtual bool Get( FString& InOutFilename, EConcurrency::Type InConcurrency = EConcurrency::Synchronous ) const override;
|
||||||
|
#else
|
||||||
|
virtual bool Get( FString& InOutFilename ) const override;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
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 = 0;
|
||||||
|
|
||||||
|
/** The index of the revision in the history (SBlueprintRevisionMenu assumes order for the "Depot" label) */
|
||||||
|
int32 RevisionNumber = 0;
|
||||||
|
|
||||||
|
/** 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;
|
||||||
|
|
||||||
|
/** Dynamic repository root **/
|
||||||
|
FString PathToRepoRoot;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** History composed of the last 100 revisions of the file */
|
||||||
|
typedef TArray< TSharedRef<FGitSourceControlRevision, ESPMode::ThreadSafe> > TGitSourceControlHistory;
|
@ -0,0 +1,95 @@
|
|||||||
|
// Copyright Project Borealis
|
||||||
|
|
||||||
|
#include "GitSourceControlRunner.h"
|
||||||
|
|
||||||
|
#include "GitSourceControlModule.h"
|
||||||
|
#include "GitSourceControlProvider.h"
|
||||||
|
#include "GitSourceControlOperations.h"
|
||||||
|
|
||||||
|
#include "Async/Async.h"
|
||||||
|
|
||||||
|
FGitSourceControlRunner::FGitSourceControlRunner()
|
||||||
|
{
|
||||||
|
bRunThread = true;
|
||||||
|
bRefreshSpawned = false;
|
||||||
|
StopEvent = FPlatformProcess::GetSynchEventFromPool(true);
|
||||||
|
Thread = FRunnableThread::Create(this, TEXT("GitSourceControlRunner"));
|
||||||
|
}
|
||||||
|
|
||||||
|
FGitSourceControlRunner::~FGitSourceControlRunner()
|
||||||
|
{
|
||||||
|
if (Thread)
|
||||||
|
{
|
||||||
|
Thread->Kill();
|
||||||
|
delete StopEvent;
|
||||||
|
delete Thread;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlRunner::Init()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32 FGitSourceControlRunner::Run()
|
||||||
|
{
|
||||||
|
while (bRunThread)
|
||||||
|
{
|
||||||
|
StopEvent->Wait(30000);
|
||||||
|
if (!bRunThread)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// If we're not running the task already
|
||||||
|
if (!bRefreshSpawned)
|
||||||
|
{
|
||||||
|
// Flag that we're running the task already
|
||||||
|
bRefreshSpawned = true;
|
||||||
|
const auto ExecuteResult = Async(EAsyncExecution::TaskGraphMainThread, [this] {
|
||||||
|
FGitSourceControlModule* GitSourceControl = FGitSourceControlModule::GetThreadSafe();
|
||||||
|
// Module not loaded, bail. Usually happens when editor is shutting down, and this prevents a crash from bad timing.
|
||||||
|
if (!GitSourceControl)
|
||||||
|
{
|
||||||
|
return ECommandResult::Failed;
|
||||||
|
}
|
||||||
|
FGitSourceControlProvider& Provider = GitSourceControl->GetProvider();
|
||||||
|
TSharedRef<FGitFetch, ESPMode::ThreadSafe> RefreshOperation = ISourceControlOperation::Create<FGitFetch>();
|
||||||
|
RefreshOperation->bUpdateStatus = true;
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
const ECommandResult::Type Result = Provider.Execute(RefreshOperation, FSourceControlChangelistPtr(), FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous,
|
||||||
|
FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlRunner::OnSourceControlOperationComplete));
|
||||||
|
#else
|
||||||
|
const ECommandResult::Type Result = Provider.Execute(RefreshOperation, FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous,
|
||||||
|
FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlRunner::OnSourceControlOperationComplete));
|
||||||
|
#endif
|
||||||
|
return Result;
|
||||||
|
});
|
||||||
|
// Wait for result if not already completed
|
||||||
|
if (bRefreshSpawned && bRunThread)
|
||||||
|
{
|
||||||
|
// Get the result
|
||||||
|
ECommandResult::Type Result = ExecuteResult.Get();
|
||||||
|
// If still not completed,
|
||||||
|
if (bRefreshSpawned)
|
||||||
|
{
|
||||||
|
// mark failures as done, successes have to complete
|
||||||
|
bRefreshSpawned = Result == ECommandResult::Succeeded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlRunner::Stop()
|
||||||
|
{
|
||||||
|
bRunThread = false;
|
||||||
|
StopEvent->Trigger();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlRunner::OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult)
|
||||||
|
{
|
||||||
|
// Mark task as done
|
||||||
|
bRefreshSpawned = false;
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
// Copyright Project Borealis
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
|
||||||
|
#include "HAL/Runnable.h"
|
||||||
|
|
||||||
|
#include "ISourceControlProvider.h"
|
||||||
|
#include "ISourceControlOperation.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class FGitSourceControlRunner : public FRunnable
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FGitSourceControlRunner();
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
virtual ~FGitSourceControlRunner() override;
|
||||||
|
|
||||||
|
bool Init() override;
|
||||||
|
uint32 Run() override;
|
||||||
|
void Stop() override;
|
||||||
|
void OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult);
|
||||||
|
|
||||||
|
|
||||||
|
private:
|
||||||
|
FRunnableThread* Thread;
|
||||||
|
FEvent* StopEvent;
|
||||||
|
bool bRunThread;
|
||||||
|
bool bRefreshSpawned;
|
||||||
|
};
|
@ -0,0 +1,86 @@
|
|||||||
|
// 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/ConfigCacheIni.h"
|
||||||
|
#include "SourceControlHelpers.h"
|
||||||
|
|
||||||
|
namespace GitSettingsConstants
|
||||||
|
{
|
||||||
|
|
||||||
|
/** The section of the ini file we load our settings from */
|
||||||
|
static const FString SettingsSection = TEXT("GitSourceControl.GitSourceControlSettings");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString FGitSourceControlSettings::GetBinaryPath() const
|
||||||
|
{
|
||||||
|
FScopeLock ScopeLock(&CriticalSection);
|
||||||
|
return BinaryPath; // Return a copy to be thread-safe
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlSettings::SetBinaryPath(const FString& InString)
|
||||||
|
{
|
||||||
|
FScopeLock ScopeLock(&CriticalSection);
|
||||||
|
const bool bChanged = (BinaryPath != InString);
|
||||||
|
if(bChanged)
|
||||||
|
{
|
||||||
|
BinaryPath = InString;
|
||||||
|
}
|
||||||
|
return bChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tell if using the Git LFS file Locking workflow */
|
||||||
|
bool FGitSourceControlSettings::IsUsingGitLfsLocking() const
|
||||||
|
{
|
||||||
|
FScopeLock ScopeLock(&CriticalSection);
|
||||||
|
return bUsingGitLfsLocking;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Configure the usage of Git LFS file Locking workflow */
|
||||||
|
bool FGitSourceControlSettings::SetUsingGitLfsLocking(const bool InUsingGitLfsLocking)
|
||||||
|
{
|
||||||
|
FScopeLock ScopeLock(&CriticalSection);
|
||||||
|
const bool bChanged = (bUsingGitLfsLocking != InUsingGitLfsLocking);
|
||||||
|
bUsingGitLfsLocking = InUsingGitLfsLocking;
|
||||||
|
return bChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString FGitSourceControlSettings::GetLfsUserName() const
|
||||||
|
{
|
||||||
|
FScopeLock ScopeLock(&CriticalSection);
|
||||||
|
return LfsUserName; // Return a copy to be thread-safe
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlSettings::SetLfsUserName(const FString& InString)
|
||||||
|
{
|
||||||
|
FScopeLock ScopeLock(&CriticalSection);
|
||||||
|
const bool bChanged = (LfsUserName != InString);
|
||||||
|
if (bChanged)
|
||||||
|
{
|
||||||
|
LfsUserName = InString;
|
||||||
|
}
|
||||||
|
return bChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is called at startup nearly before anything else in our module: BinaryPath will then be used by the provider
|
||||||
|
void FGitSourceControlSettings::LoadSettings()
|
||||||
|
{
|
||||||
|
FScopeLock ScopeLock(&CriticalSection);
|
||||||
|
const FString& IniFile = SourceControlHelpers::GetSettingsIni();
|
||||||
|
GConfig->GetString(*GitSettingsConstants::SettingsSection, TEXT("BinaryPath"), BinaryPath, IniFile);
|
||||||
|
GConfig->GetBool(*GitSettingsConstants::SettingsSection, TEXT("UsingGitLfsLocking"), bUsingGitLfsLocking, IniFile);
|
||||||
|
GConfig->GetString(*GitSettingsConstants::SettingsSection, TEXT("LfsUserName"), LfsUserName, IniFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FGitSourceControlSettings::SaveSettings() const
|
||||||
|
{
|
||||||
|
FScopeLock ScopeLock(&CriticalSection);
|
||||||
|
const FString& IniFile = SourceControlHelpers::GetSettingsIni();
|
||||||
|
GConfig->SetString(*GitSettingsConstants::SettingsSection, TEXT("BinaryPath"), *BinaryPath, IniFile);
|
||||||
|
GConfig->SetBool(*GitSettingsConstants::SettingsSection, TEXT("UsingGitLfsLocking"), bUsingGitLfsLocking, IniFile);
|
||||||
|
GConfig->SetString(*GitSettingsConstants::SettingsSection, TEXT("LfsUserName"), *LfsUserName, IniFile);
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com)
|
||||||
|
//
|
||||||
|
// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
|
||||||
|
// or copy at http://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Containers/UnrealString.h"
|
||||||
|
#include "HAL/CriticalSection.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 = true;
|
||||||
|
|
||||||
|
/** Username used by the Git LFS 2 File Locks server */
|
||||||
|
FString LfsUserName;
|
||||||
|
};
|
@ -0,0 +1,461 @@
|
|||||||
|
// 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"
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
#include "Textures/SlateIcon.h"
|
||||||
|
#if ENGINE_MINOR_VERSION >= 2
|
||||||
|
#include "RevisionControlStyle/RevisionControlStyle.h"
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#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 (auto Iter(History.CreateConstIterator()); Iter; Iter++)
|
||||||
|
{
|
||||||
|
if ((*Iter)->GetRevisionNumber() == RevisionNumber)
|
||||||
|
{
|
||||||
|
return *Iter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION < 5 || ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 3
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2
|
||||||
|
TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FGitSourceControlState::GetCurrentRevision() const
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
|
||||||
|
ISourceControlState::FResolveInfo FGitSourceControlState::GetResolveInfo() const
|
||||||
|
{
|
||||||
|
return PendingResolveInfo;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// @todo add Slate icons for git specific states (NotAtHead vs Conflicted...)
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION < 5
|
||||||
|
#define GET_ICON_RETURN( NAME ) FName( "ContentBrowser.SCC_" #NAME )
|
||||||
|
FName FGitSourceControlState::GetIconName() const
|
||||||
|
{
|
||||||
|
#else
|
||||||
|
#if ENGINE_MINOR_VERSION >= 2
|
||||||
|
#define GET_ICON_RETURN( NAME ) FSlateIcon(FRevisionControlStyleManager::GetStyleSetName(), "RevisionControl." #NAME )
|
||||||
|
#else
|
||||||
|
#define GET_ICON_RETURN( NAME ) FSlateIcon(FAppStyle::GetAppStyleSetName(), "Perforce." #NAME )
|
||||||
|
#endif
|
||||||
|
FSlateIcon FGitSourceControlState::GetIcon() const
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
switch (GetGitState())
|
||||||
|
{
|
||||||
|
case EGitState::NotAtHead:
|
||||||
|
return GET_ICON_RETURN(NotAtHeadRevision);
|
||||||
|
case EGitState::LockedOther:
|
||||||
|
return GET_ICON_RETURN(CheckedOutByOtherUser);
|
||||||
|
case EGitState::NotLatest:
|
||||||
|
return GET_ICON_RETURN(ModifiedOtherBranch);
|
||||||
|
case EGitState::Unmerged:
|
||||||
|
return GET_ICON_RETURN(Branched);
|
||||||
|
case EGitState::Added:
|
||||||
|
return GET_ICON_RETURN(OpenForAdd);
|
||||||
|
case EGitState::Untracked:
|
||||||
|
return GET_ICON_RETURN(NotInDepot);
|
||||||
|
case EGitState::Deleted:
|
||||||
|
return GET_ICON_RETURN(MarkedForDelete);
|
||||||
|
case EGitState::Modified:
|
||||||
|
case EGitState::CheckedOut:
|
||||||
|
return GET_ICON_RETURN(CheckedOut);
|
||||||
|
case EGitState::Ignored:
|
||||||
|
return GET_ICON_RETURN(NotInDepot);
|
||||||
|
default:
|
||||||
|
#if ENGINE_MAJOR_VERSION < 5
|
||||||
|
return NAME_None;
|
||||||
|
#else
|
||||||
|
return FSlateIcon();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION < 5
|
||||||
|
FName FGitSourceControlState::GetSmallIconName() const
|
||||||
|
{
|
||||||
|
switch (GetGitState()) {
|
||||||
|
case EGitState::NotAtHead:
|
||||||
|
return FName("ContentBrowser.SCC_NotAtHeadRevision_Small");
|
||||||
|
case EGitState::LockedOther:
|
||||||
|
return FName("ContentBrowser.SCC_CheckedOutByOtherUser_Small");
|
||||||
|
case EGitState::NotLatest:
|
||||||
|
return FName("ContentBrowser.SCC_ModifiedOtherBranch_Small");
|
||||||
|
case EGitState::Unmerged:
|
||||||
|
return FName("ContentBrowser.SCC_Branched_Small");
|
||||||
|
case EGitState::Added:
|
||||||
|
return FName("ContentBrowser.SCC_OpenForAdd_Small");
|
||||||
|
case EGitState::Untracked:
|
||||||
|
return FName("ContentBrowser.SCC_NotInDepot_Small");
|
||||||
|
case EGitState::Deleted:
|
||||||
|
return FName("ContentBrowser.SCC_MarkedForDelete_Small");
|
||||||
|
case EGitState::Modified:
|
||||||
|
case EGitState::CheckedOut:
|
||||||
|
return FName("ContentBrowser.SCC_CheckedOut_Small");
|
||||||
|
case EGitState::Ignored:
|
||||||
|
return FName("ContentBrowser.SCC_NotInDepot_Small");
|
||||||
|
default:
|
||||||
|
return NAME_None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
FText FGitSourceControlState::GetDisplayName() const
|
||||||
|
{
|
||||||
|
switch (GetGitState())
|
||||||
|
{
|
||||||
|
case EGitState::NotAtHead:
|
||||||
|
return LOCTEXT("NotCurrent", "Not current");
|
||||||
|
case EGitState::LockedOther:
|
||||||
|
return FText::Format(LOCTEXT("CheckedOutOther", "Checked out by: {0}"), FText::FromString(State.LockUser));
|
||||||
|
case EGitState::NotLatest:
|
||||||
|
return FText::Format(LOCTEXT("ModifiedOtherBranch", "Modified in branch: {0}"), FText::FromString(State.HeadBranch));
|
||||||
|
case EGitState::Unmerged:
|
||||||
|
return LOCTEXT("Conflicted", "Conflicted");
|
||||||
|
case EGitState::Added:
|
||||||
|
return LOCTEXT("OpenedForAdd", "Opened for add");
|
||||||
|
case EGitState::Untracked:
|
||||||
|
return LOCTEXT("NotControlled", "Not Under Revision Control");
|
||||||
|
case EGitState::Deleted:
|
||||||
|
return LOCTEXT("MarkedForDelete", "Marked for delete");
|
||||||
|
case EGitState::Modified:
|
||||||
|
case EGitState::CheckedOut:
|
||||||
|
return LOCTEXT("CheckedOut", "Checked out");
|
||||||
|
case EGitState::Ignored:
|
||||||
|
return LOCTEXT("Ignore", "Ignore");
|
||||||
|
case EGitState::Lockable:
|
||||||
|
return LOCTEXT("ReadOnly", "Read only");
|
||||||
|
case EGitState::None:
|
||||||
|
return LOCTEXT("Unknown", "Unknown");
|
||||||
|
default:
|
||||||
|
return FText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FText FGitSourceControlState::GetDisplayTooltip() const
|
||||||
|
{
|
||||||
|
switch (GetGitState())
|
||||||
|
{
|
||||||
|
case EGitState::NotAtHead:
|
||||||
|
return LOCTEXT("NotCurrent_Tooltip", "The file(s) are not at the head revision");
|
||||||
|
case EGitState::LockedOther:
|
||||||
|
return FText::Format(LOCTEXT("CheckedOutOther_Tooltip", "Checked out by: {0}"), FText::FromString(State.LockUser));
|
||||||
|
case EGitState::NotLatest:
|
||||||
|
return FText::Format(LOCTEXT("ModifiedOtherBranch_Tooltip", "Modified in branch: {0} CL:{1} ({2})"), FText::FromString(State.HeadBranch), FText::FromString(HeadCommit), FText::FromString(HeadAction));
|
||||||
|
case EGitState::Unmerged:
|
||||||
|
return LOCTEXT("ContentsConflict_Tooltip", "The contents of the item conflict with updates received from the repository.");
|
||||||
|
case EGitState::Added:
|
||||||
|
return LOCTEXT("OpenedForAdd_Tooltip", "The file(s) are opened for add");
|
||||||
|
case EGitState::Untracked:
|
||||||
|
return LOCTEXT("NotControlled_Tooltip", "Item is not under revision control.");
|
||||||
|
case EGitState::Deleted:
|
||||||
|
return LOCTEXT("MarkedForDelete_Tooltip", "The file(s) are marked for delete");
|
||||||
|
case EGitState::Modified:
|
||||||
|
case EGitState::CheckedOut:
|
||||||
|
return LOCTEXT("CheckedOut_Tooltip", "The file(s) are checked out");
|
||||||
|
case EGitState::Ignored:
|
||||||
|
return LOCTEXT("Ignored_Tooltip", "Item is being ignored.");
|
||||||
|
case EGitState::Lockable:
|
||||||
|
return LOCTEXT("ReadOnly_Tooltip", "The file(s) are marked locally as read-only");
|
||||||
|
case EGitState::None:
|
||||||
|
return LOCTEXT("Unknown_Tooltip", "Unknown revision control state");
|
||||||
|
default:
|
||||||
|
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 they do in the Submit files to Revision Control window!
|
||||||
|
bool FGitSourceControlState::CanCheckIn() const
|
||||||
|
{
|
||||||
|
// We can check in if this is new content
|
||||||
|
if (IsAdded())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot check back in if conflicted or not current
|
||||||
|
if (!IsCurrent() || IsConflicted())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can check back in if we're locked.
|
||||||
|
if (State.LockState == ELockState::Locked)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can check in any file that has been modified, unless someone else locked it.
|
||||||
|
if (State.LockState != ELockState::LockedOther && IsModified() && IsSourceControlled())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlState::CanCheckout() const
|
||||||
|
{
|
||||||
|
if (State.LockState == ELockState::Unlockable)
|
||||||
|
{
|
||||||
|
// Everything is already available for check in (checked out).
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 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 State.LockState == ELockState::NotLocked && IsCurrent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlState::IsCheckedOut() const
|
||||||
|
{
|
||||||
|
if (State.LockState == ELockState::Unlockable)
|
||||||
|
{
|
||||||
|
return IsSourceControlled(); // TODO: try modified instead? might block editing the file with a holding pattern
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We check for modified here too, because sometimes you don't lock a file but still want to push it. CanCheckout still true, so that you can lock it later...
|
||||||
|
return State.LockState == ELockState::Locked || (State.FileState == EFileState::Modified && State.LockState != ELockState::LockedOther);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlState::IsCheckedOutOther(FString* Who) const
|
||||||
|
{
|
||||||
|
if (Who != nullptr)
|
||||||
|
{
|
||||||
|
// The packages dialog uses our lock user regardless if it was locked by other or us.
|
||||||
|
// But, if there is no lock user, it shows information about modification in other branches, which is important.
|
||||||
|
// So, only show our own lock user if it hasn't been modified in another branch.
|
||||||
|
// This is a very, very rare state (maybe impossible), but one that should be displayed properly.
|
||||||
|
if (State.LockState == ELockState::LockedOther || (State.LockState == ELockState::Locked && !IsModifiedInOtherBranch()))
|
||||||
|
{
|
||||||
|
*Who = State.LockUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return State.LockState == ELockState::LockedOther;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlState::IsCheckedOutInOtherBranch(const FString& CurrentBranch) const
|
||||||
|
{
|
||||||
|
// You can't check out separately per branch
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlState::IsModifiedInOtherBranch(const FString& CurrentBranch) const
|
||||||
|
{
|
||||||
|
return State.RemoteState == ERemoteState::NotLatest;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlState::GetOtherBranchHeadModification(FString& HeadBranchOut, FString& ActionOut, int32& HeadChangeListOut) const
|
||||||
|
{
|
||||||
|
if (!IsModifiedInOtherBranch())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HeadBranchOut = State.HeadBranch;
|
||||||
|
ActionOut = HeadAction; // TODO: from ERemoteState
|
||||||
|
HeadChangeListOut = 0; // TODO: get head commit
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlState::IsCurrent() const
|
||||||
|
{
|
||||||
|
return State.RemoteState != ERemoteState::NotAtHead && State.RemoteState != ERemoteState::NotLatest;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlState::IsSourceControlled() const
|
||||||
|
{
|
||||||
|
return State.TreeState != ETreeState::Untracked && State.TreeState != ETreeState::Ignored && State.TreeState != ETreeState::NotInRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlState::IsAdded() const
|
||||||
|
{
|
||||||
|
// Added is when a file was untracked and is now added.
|
||||||
|
return State.FileState == EFileState::Added;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlState::IsDeleted() const
|
||||||
|
{
|
||||||
|
return State.FileState == EFileState::Deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlState::IsIgnored() const
|
||||||
|
{
|
||||||
|
return State.TreeState == ETreeState::Ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlState::CanEdit() const
|
||||||
|
{
|
||||||
|
// Perforce does not care about it being current
|
||||||
|
return IsCheckedOut() || IsAdded();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlState::CanDelete() const
|
||||||
|
{
|
||||||
|
// Perforce enforces that a deleted file must be current.
|
||||||
|
if (!IsCurrent())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// If someone else hasn't checked it out, we can delete revision controlled files.
|
||||||
|
return !IsCheckedOutOther() && IsSourceControlled();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlState::IsUnknown() const
|
||||||
|
{
|
||||||
|
return State.FileState == EFileState::Unknown && State.TreeState == ETreeState::NotInRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlState::IsModified() const
|
||||||
|
{
|
||||||
|
return State.TreeState == ETreeState::Working ||
|
||||||
|
State.TreeState == ETreeState::Staged;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool FGitSourceControlState::CanAdd() const
|
||||||
|
{
|
||||||
|
return State.TreeState == ETreeState::Untracked;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlState::IsConflicted() const
|
||||||
|
{
|
||||||
|
return State.FileState == EFileState::Unmerged;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FGitSourceControlState::CanRevert() const
|
||||||
|
{
|
||||||
|
// Can revert the file state if we modified, even if it was locked by someone else.
|
||||||
|
// Useful for when someone locked a file, and you just wanna play around with it locallly, and then revert it.
|
||||||
|
return CanCheckIn() || IsModified();
|
||||||
|
}
|
||||||
|
|
||||||
|
EGitState::Type FGitSourceControlState::GetGitState() const
|
||||||
|
{
|
||||||
|
// No matter what, we must pull from remote, even if we have locked or if we have modified.
|
||||||
|
switch (State.RemoteState)
|
||||||
|
{
|
||||||
|
case ERemoteState::NotAtHead:
|
||||||
|
return EGitState::NotAtHead;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Someone else locked this file across branches. */
|
||||||
|
// We cannot push under any circumstance, if someone else has locked.
|
||||||
|
if (State.LockState == ELockState::LockedOther)
|
||||||
|
{
|
||||||
|
return EGitState::LockedOther;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We could theoretically push, but we shouldn't.
|
||||||
|
if (State.RemoteState == ERemoteState::NotLatest)
|
||||||
|
{
|
||||||
|
return EGitState::NotLatest;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (State.FileState)
|
||||||
|
{
|
||||||
|
case EFileState::Unmerged:
|
||||||
|
return EGitState::Unmerged;
|
||||||
|
case EFileState::Added:
|
||||||
|
return EGitState::Added;
|
||||||
|
case EFileState::Deleted:
|
||||||
|
return EGitState::Deleted;
|
||||||
|
case EFileState::Modified:
|
||||||
|
return EGitState::Modified;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (State.TreeState == ETreeState::Untracked)
|
||||||
|
{
|
||||||
|
return EGitState::Untracked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (State.LockState == ELockState::Locked)
|
||||||
|
{
|
||||||
|
return EGitState::CheckedOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsSourceControlled())
|
||||||
|
{
|
||||||
|
if (CanCheckout())
|
||||||
|
{
|
||||||
|
return EGitState::Lockable;
|
||||||
|
}
|
||||||
|
return EGitState::Unmodified;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EGitState::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
@ -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 "GitSourceControlChangelist.h"
|
||||||
|
#include "GitSourceControlRevision.h"
|
||||||
|
#include "Runtime/Launch/Resources/Version.h"
|
||||||
|
|
||||||
|
/** A consolidation of state priorities. */
|
||||||
|
namespace EGitState
|
||||||
|
{
|
||||||
|
enum Type
|
||||||
|
{
|
||||||
|
Unset,
|
||||||
|
NotAtHead,
|
||||||
|
#if 0
|
||||||
|
AddedAtHead,
|
||||||
|
DeletedAtHead,
|
||||||
|
#endif
|
||||||
|
LockedOther,
|
||||||
|
NotLatest,
|
||||||
|
/** Unmerged state (modified, but conflicts) */
|
||||||
|
Unmerged,
|
||||||
|
Added,
|
||||||
|
Deleted,
|
||||||
|
Modified,
|
||||||
|
/** Not modified, but locked explicitly. */
|
||||||
|
CheckedOut,
|
||||||
|
Untracked,
|
||||||
|
Lockable,
|
||||||
|
Unmodified,
|
||||||
|
Ignored,
|
||||||
|
/** Whatever else. */
|
||||||
|
None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Corresponds to diff file states. */
|
||||||
|
namespace EFileState
|
||||||
|
{
|
||||||
|
enum Type
|
||||||
|
{
|
||||||
|
Unset,
|
||||||
|
Unknown,
|
||||||
|
Added,
|
||||||
|
Copied,
|
||||||
|
Deleted,
|
||||||
|
Modified,
|
||||||
|
Renamed,
|
||||||
|
Missing,
|
||||||
|
Unmerged,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Where in the world is this file? */
|
||||||
|
namespace ETreeState
|
||||||
|
{
|
||||||
|
enum Type
|
||||||
|
{
|
||||||
|
Unset,
|
||||||
|
/** This file is synced to commit */
|
||||||
|
Unmodified,
|
||||||
|
/** This file is modified, but not in staging tree */
|
||||||
|
Working,
|
||||||
|
/** This file is in staging tree (git add) */
|
||||||
|
Staged,
|
||||||
|
/** This file is not tracked in the repo yet */
|
||||||
|
Untracked,
|
||||||
|
/** This file is ignored by the repo */
|
||||||
|
Ignored,
|
||||||
|
/** This file is outside the repo folder */
|
||||||
|
NotInRepo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** LFS locks status of this file */
|
||||||
|
namespace ELockState
|
||||||
|
{
|
||||||
|
enum Type
|
||||||
|
{
|
||||||
|
Unset,
|
||||||
|
Unknown,
|
||||||
|
Unlockable,
|
||||||
|
NotLocked,
|
||||||
|
Locked,
|
||||||
|
LockedOther,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What is this file doing at HEAD? */
|
||||||
|
namespace ERemoteState
|
||||||
|
{
|
||||||
|
enum Type
|
||||||
|
{
|
||||||
|
Unset,
|
||||||
|
/** Up to date */
|
||||||
|
UpToDate,
|
||||||
|
/** Local version is behind remote */
|
||||||
|
NotAtHead,
|
||||||
|
#if 0
|
||||||
|
// TODO: handle these
|
||||||
|
/** Remote file does not exist on local */
|
||||||
|
AddedAtHead,
|
||||||
|
/** Local was deleted on remote */
|
||||||
|
DeletedAtHead,
|
||||||
|
#endif
|
||||||
|
/** Not at the latest revision amongst the tracked branches */
|
||||||
|
NotLatest,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Combined state, for updating cache in a map. */
|
||||||
|
struct FGitState
|
||||||
|
{
|
||||||
|
EFileState::Type FileState = EFileState::Unknown;
|
||||||
|
ETreeState::Type TreeState = ETreeState::NotInRepo;
|
||||||
|
ELockState::Type LockState = ELockState::Unknown;
|
||||||
|
/** Name of user who has locked the file */
|
||||||
|
FString LockUser;
|
||||||
|
ERemoteState::Type RemoteState = ERemoteState::UpToDate;
|
||||||
|
/** The branch with the latest commit for this file */
|
||||||
|
FString HeadBranch;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FGitSourceControlState : public ISourceControlState
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FGitSourceControlState(const FString &InLocalFilename)
|
||||||
|
: LocalFilename(InLocalFilename)
|
||||||
|
, TimeStamp(0)
|
||||||
|
, HeadAction(TEXT("Changed"))
|
||||||
|
, HeadCommit(TEXT("Unknown"))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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;
|
||||||
|
#if ENGINE_MAJOR_VERSION < 5 || ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 3
|
||||||
|
virtual TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> GetBaseRevForMerge() const override;
|
||||||
|
#else
|
||||||
|
virtual FResolveInfo GetResolveInfo() const override;
|
||||||
|
#endif
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2
|
||||||
|
virtual TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> GetCurrentRevision() const override;
|
||||||
|
#endif
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
virtual FSlateIcon GetIcon() const override;
|
||||||
|
#else
|
||||||
|
virtual FName GetIconName() const override;
|
||||||
|
virtual FName GetSmallIconName() const override;
|
||||||
|
#endif
|
||||||
|
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 = NULL) const override;
|
||||||
|
virtual bool IsCheckedOutInOtherBranch(const FString& CurrentBranch = FString()) const override;
|
||||||
|
virtual bool IsModifiedInOtherBranch(const FString& CurrentBranch = FString()) const override;
|
||||||
|
virtual bool IsCheckedOutOrModifiedInOtherBranch(const FString& CurrentBranch = FString()) const override { return IsModifiedInOtherBranch(CurrentBranch); }
|
||||||
|
virtual TArray<FString> GetCheckedOutBranches() const override { return TArray<FString>(); }
|
||||||
|
virtual FString GetOtherUserBranchCheckedOuts() const override { return FString(); }
|
||||||
|
virtual bool GetOtherBranchHeadModification(FString& HeadBranchOut, FString& ActionOut, int32& HeadChangeListOut) const override;
|
||||||
|
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 IsUnknown() const override;
|
||||||
|
virtual bool IsModified() const override;
|
||||||
|
virtual bool CanAdd() const override;
|
||||||
|
virtual bool CanDelete() const override;
|
||||||
|
virtual bool IsConflicted() const override;
|
||||||
|
virtual bool CanRevert() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
EGitState::Type GetGitState() const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** History of the item, if any */
|
||||||
|
TGitSourceControlHistory History;
|
||||||
|
|
||||||
|
/** Filename on disk */
|
||||||
|
FString LocalFilename;
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
|
||||||
|
/** Pending rev info with which a file must be resolved, invalid if no resolve pending */
|
||||||
|
FResolveInfo PendingResolveInfo;
|
||||||
|
|
||||||
|
UE_DEPRECATED(5.3, "Use PendingResolveInfo.BaseRevision instead")
|
||||||
|
#endif
|
||||||
|
/** File Id with which our local revision diverged from the remote revision */
|
||||||
|
FString PendingMergeBaseFileHash;
|
||||||
|
|
||||||
|
/** Status of the file */
|
||||||
|
FGitState State;
|
||||||
|
|
||||||
|
FGitSourceControlChangelist Changelist;
|
||||||
|
|
||||||
|
/** The timestamp of the last update */
|
||||||
|
FDateTime TimeStamp;
|
||||||
|
|
||||||
|
/** The action within the head branch TODO */
|
||||||
|
FString HeadAction;
|
||||||
|
|
||||||
|
/** The last file modification time in the head branch TODO */
|
||||||
|
int64 HeadModTime;
|
||||||
|
|
||||||
|
/** The change list the last modification TODO */
|
||||||
|
FString HeadCommit;
|
||||||
|
};
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,367 @@
|
|||||||
|
// 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 "GitSourceControlRevision.h"
|
||||||
|
#include "GitSourceControlState.h"
|
||||||
|
|
||||||
|
class FGitSourceControlState;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
class FGitLockedFilesCache
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static FDateTime LastUpdated;
|
||||||
|
|
||||||
|
static const TMap<FString, FString>& GetLockedFiles() { return LockedFiles; }
|
||||||
|
static void SetLockedFiles(const TMap<FString, FString>& newLocks);
|
||||||
|
static void AddLockedFile(const FString& filePath, const FString& lockUser);
|
||||||
|
static void RemoveLockedFile(const FString& filePath);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void OnFileLockChanged(const FString& filePath, const FString& lockUser, bool locked);
|
||||||
|
// update local read/write state when our own lock statuses change
|
||||||
|
static TMap<FString, FString> LockedFiles;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace GitSourceControlUtils
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns an updated repo root if all selected files are in a plugin subfolder, and the plugin subfolder is a git repo
|
||||||
|
* This supports the case where each plugin is a sub module
|
||||||
|
*
|
||||||
|
* @param AbsoluteFilePaths The list of files in the SC operation
|
||||||
|
* @param PathToRepositoryRoot The original path to the repository root (used by default)
|
||||||
|
*/
|
||||||
|
FString ChangeRepositoryRootIfSubmodule(const TArray<FString>& AbsoluteFilePaths, const FString& PathToRepositoryRoot);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an updated repo root if all selected file is in a plugin subfolder, and the plugin subfolder is a git repo
|
||||||
|
* This supports the case where each plugin is a sub module
|
||||||
|
*
|
||||||
|
* @param AbsoluteFilePath The file in the SC operation
|
||||||
|
* @param PathToRepositoryRoot The original path to the repository root (used by default)
|
||||||
|
*/
|
||||||
|
FString ChangeRepositoryRootIfSubmodule(const FString& AbsoluteFilePath, const FString& PathToRepositoryRoot);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the path to the Git binary, looking into a few places (standalone Git install, and other common tools embedding Git)
|
||||||
|
* @returns the path to the Git binary if found, or an empty string.
|
||||||
|
*/
|
||||||
|
FString FindGitBinaryPath();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 remote tracking branch
|
||||||
|
* @returns false if the branch is not tracking a remote
|
||||||
|
*/
|
||||||
|
bool GetRemoteBranchName(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutBranchName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Git remote tracking branches that match wildcard
|
||||||
|
* @returns false if no matching branches
|
||||||
|
*/
|
||||||
|
bool GetRemoteBranchesWildcard(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& PatternMatch, TArray<FString>& OutBranchNames);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Git current commit details
|
||||||
|
* @param InPathToGitBinary The path to the Git binary
|
||||||
|
* @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory
|
||||||
|
* @param OutCommitId Current Commit full SHA1
|
||||||
|
* @param OutCommitSummary Current Commit description's Summary
|
||||||
|
* @returns true if the command succeeded and returned no errors
|
||||||
|
*/
|
||||||
|
bool GetCommitInfo(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutCommitId, FString& OutCommitSummary);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get 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);
|
||||||
|
bool RunCommandInternalRaw(const FString& InCommand, const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray<FString>& InParameters, const TArray<FString>& InFiles, FString& OutResults, FString& OutErrors, const int32 ExpectedReturnCode = 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unloads packages of specified named files
|
||||||
|
*/
|
||||||
|
TArray<class UPackage*> UnlinkPackages(const TArray<FString>& InPackageNames);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads packages for these packages
|
||||||
|
*/
|
||||||
|
void ReloadPackages(TArray<UPackage*>& InPackagesToReload);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all Git tracked files, including within directories, recursively
|
||||||
|
*/
|
||||||
|
bool ListFilesInDirectoryRecurse(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InDirectory, TArray<FString>& OutFiles);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks remote branches to see file differences.
|
||||||
|
*
|
||||||
|
* @param CurrentBranchName The current branch we are on.
|
||||||
|
* @param InPathToGitBinary The path to the Git binary
|
||||||
|
* @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory
|
||||||
|
* @param OnePath The file to be checked
|
||||||
|
* @param OutErrorMessages Any errors (from StdErr) as an array per-line
|
||||||
|
*/
|
||||||
|
void CheckRemote(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray<FString>& Files,
|
||||||
|
TArray<FString>& OutErrorMessages, TMap<FString, FGitSourceControlState>& OutStates);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a Git "status" command and parse it.
|
||||||
|
*
|
||||||
|
* @param InPathToGitBinary The path to the Git binary
|
||||||
|
* @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory (can be empty)
|
||||||
|
* @param InUsingLfsLocking Tells if using the Git LFS file Locking workflow
|
||||||
|
* @param InFiles The files to be operated on
|
||||||
|
* @param OutErrorMessages Any errors (from StdErr) as an array per-line
|
||||||
|
* @param OutStates The resultant states
|
||||||
|
* @returns true if the command succeeded and returned no errors
|
||||||
|
*/
|
||||||
|
bool RunUpdateStatus(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool InUsingLfsLocking, const TArray<FString>& InFiles,
|
||||||
|
TArray<FString>& OutErrorMessages, TMap<FString, FGitSourceControlState>& OutStates);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep Consistency of being file staged
|
||||||
|
*
|
||||||
|
* @param Filename Saved filename
|
||||||
|
* @param Pkg Package (for adapting delegate)
|
||||||
|
* @param ObjectSaveContext Context for save (for adapting delegate)
|
||||||
|
*/
|
||||||
|
void UpdateFileStagingOnSaved(const FString& Filename, UPackage* Pkg, FObjectPostSaveContext ObjectSaveContext);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep Consistency of being file staged with simple argument
|
||||||
|
*
|
||||||
|
* @param Filename Saved filename
|
||||||
|
*/
|
||||||
|
bool UpdateFileStagingOnSavedInternal(const FString& Filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param Filename Saved filename
|
||||||
|
* @param Pkg Package (for adapting delegate)
|
||||||
|
* @param ObjectSaveContext Context for save (for adapting delegate)
|
||||||
|
*/
|
||||||
|
void UpdateStateOnAssetRename(const FAssetData& InAssetData, const FString& InOldName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param Filename Saved filename
|
||||||
|
* @param Pkg Package (for adapting delegate)
|
||||||
|
* @param ObjectSaveContext Context for save (for adapting delegate)
|
||||||
|
*/
|
||||||
|
bool UpdateChangelistStateByCommand();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a Git "cat-file" command to dump the binary content of a revision into a file.
|
||||||
|
*
|
||||||
|
* @param InPathToGitBinary The path to the Git binary
|
||||||
|
* @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory
|
||||||
|
* @param InParameter The parameters to the Git show command (rev:path)
|
||||||
|
* @param InDumpFileName The temporary file to dump the revision
|
||||||
|
* @returns true if the command succeeded and returned no errors
|
||||||
|
*/
|
||||||
|
bool RunDumpToFile(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InParameter, const FString& InDumpFileName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a Git "log" command and parse it.
|
||||||
|
*
|
||||||
|
* @param InPathToGitBinary The path to the Git binary
|
||||||
|
* @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory
|
||||||
|
* @param InFile The file to be operated on
|
||||||
|
* @param bMergeConflict In case of a merge conflict, we also need to get the tip of the "remote branch" (MERGE_HEAD) before the log of the "current branch" (HEAD)
|
||||||
|
* @param OutErrorMessages Any errors (from StdErr) as an array per-line
|
||||||
|
* @param OutHistory The history of the file
|
||||||
|
*/
|
||||||
|
bool RunGetHistory(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InFile, bool bMergeConflict, TArray<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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
bool RunLFSCommand(const FString& InCommand, const FString& InRepositoryRoot, const FString& GitBinaryFallback, const TArray<FString>& InParameters, const TArray<FString>& InFiles, TArray<FString>& OutResults, TArray<FString>& OutErrorMessages);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function for various commands to update cached states.
|
||||||
|
* @returns true if any states were updated
|
||||||
|
*/
|
||||||
|
bool UpdateCachedStates(const TMap<const FString, FGitState>& InResults);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function for various commands to collect new states.
|
||||||
|
* @returns true if any states were updated
|
||||||
|
*/
|
||||||
|
bool CollectNewStates(const TMap<FString, FGitSourceControlState>& InStates, TMap<const FString, FGitState>& OutResults);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function for various commands to collect new states.
|
||||||
|
* @returns true if any states were updated
|
||||||
|
*/
|
||||||
|
bool CollectNewStates(const TArray<FString>& InFiles, TMap<const FString, FGitState>& OutResults, EFileState::Type FileState, ETreeState::Type TreeState = ETreeState::Unset, ELockState::Type LockState = ELockState::Unset, ERemoteState::Type RemoteState = ERemoteState::Unset);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run 'git lfs locks" to extract all lock information for all files in the repository
|
||||||
|
*
|
||||||
|
* @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory
|
||||||
|
* @param GitBinaryFallBack The Git binary fallback path
|
||||||
|
* @param OutErrorMessages Any errors (from StdErr) as an array per-line
|
||||||
|
* @param OutLocks The lock results (file, username)
|
||||||
|
* @returns true if the command succeeded and returned no errors
|
||||||
|
*/
|
||||||
|
bool GetAllLocks(const FString& InRepositoryRoot, const FString& GitBinaryFallBack, TArray<FString>& OutErrorMessages, TMap<FString, FString>& OutLocks, bool bInvalidateCache = false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets locks from state cache
|
||||||
|
*/
|
||||||
|
void GetLockedFiles(const TArray<FString>& InFiles, TArray<FString>& OutFiles);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks cache for if this file type is lockable
|
||||||
|
*/
|
||||||
|
bool IsFileLFSLockable(const FString& InFile);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets Git attribute to see if these extensions are lockable
|
||||||
|
*/
|
||||||
|
bool CheckLFSLockable(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray<FString>& InFiles, TArray<FString>& OutErrorMessages);
|
||||||
|
|
||||||
|
bool FetchRemote(const FString& InPathToGitBinary, const FString& InPathToRepositoryRoot, bool InUsingGitLfsLocking, TArray<FString>& OutResults, TArray<FString>& OutErrorMessages);
|
||||||
|
|
||||||
|
bool PullOrigin(const FString& InPathToGitBinary, const FString& InPathToRepositoryRoot, const TArray<FString>& InFiles, TArray<FString>& OutFiles,
|
||||||
|
TArray<FString>& OutResults, TArray<FString>& OutErrorMessages);
|
||||||
|
|
||||||
|
|
||||||
|
TSharedPtr< class ISourceControlRevision, ESPMode::ThreadSafe > GetOriginRevisionOnBranch( const FString & InPathToGitBinary, const FString & InRepositoryRoot, const FString & InRelativeFileName, TArray< FString > & OutErrorMessages, const FString & BranchName );
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
// 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 "Templates/SharedPointer.h"
|
||||||
|
|
||||||
|
class IGitSourceControlWorker
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name describing the work that this worker does. Used for factory method hookup.
|
||||||
|
*/
|
||||||
|
virtual FName GetName() const = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function that actually does the work. Can be executed on another thread.
|
||||||
|
*/
|
||||||
|
virtual bool Execute( class FGitSourceControlCommand& InCommand ) = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the state of any items after completion (if necessary). This is always executed on the main thread.
|
||||||
|
* @returns true if states were updated
|
||||||
|
*/
|
||||||
|
virtual bool UpdateStates() const = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef TSharedRef<IGitSourceControlWorker, ESPMode::ThreadSafe> FGitSourceControlWorkerRef;
|
@ -0,0 +1,982 @@
|
|||||||
|
// 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 "Runtime/Launch/Resources/Version.h"
|
||||||
|
#include "Fonts/SlateFontInfo.h"
|
||||||
|
#include "Misc/App.h"
|
||||||
|
#include "Misc/FileHelper.h"
|
||||||
|
#include "Misc/Paths.h"
|
||||||
|
#include "Modules/ModuleManager.h"
|
||||||
|
#include "Widgets/SBoxPanel.h"
|
||||||
|
#include "Widgets/Text/STextBlock.h"
|
||||||
|
#include "Widgets/Input/SButton.h"
|
||||||
|
#include "Widgets/Input/SEditableTextBox.h"
|
||||||
|
#include "Widgets/Input/SFilePathPicker.h"
|
||||||
|
#include "Widgets/Input/SMultiLineEditableTextBox.h"
|
||||||
|
#include "Widgets/Layout/SSeparator.h"
|
||||||
|
#include "Widgets/Notifications/SNotificationList.h"
|
||||||
|
#include "Framework/Notifications/NotificationManager.h"
|
||||||
|
#include "EditorDirectories.h"
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
|
||||||
|
#else
|
||||||
|
#include "EditorStyleSet.h"
|
||||||
|
#endif
|
||||||
|
#include "SourceControlOperations.h"
|
||||||
|
#include "GitSourceControlModule.h"
|
||||||
|
#include "GitSourceControlUtils.h"
|
||||||
|
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "SGitSourceControlSettings"
|
||||||
|
|
||||||
|
void SGitSourceControlSettings::Construct(const FArguments& InArgs)
|
||||||
|
{
|
||||||
|
bAutoCreateGitIgnore = true;
|
||||||
|
bAutoCreateReadme = true;
|
||||||
|
bAutoCreateGitAttributes = false;
|
||||||
|
bAutoInitialCommit = true;
|
||||||
|
|
||||||
|
InitialCommitMessage = LOCTEXT("InitialCommitMessage", "Initial commit");
|
||||||
|
ReadmeContent = FText::FromString(FString(TEXT("# ")) + FApp::GetProjectName() + "\n\nDeveloped with Unreal Engine\n");
|
||||||
|
|
||||||
|
ConstructBasedOnEngineVersion( );
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ENGINE_MAJOR_VERSION < 5
|
||||||
|
void SGitSourceControlSettings::ConstructBasedOnEngineVersion( )
|
||||||
|
{
|
||||||
|
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
|
||||||
|
|
||||||
|
const FSlateFontInfo Font = FEditorStyle::GetFontStyle(TEXT("SourceControl.LoginWindow.Font"));
|
||||||
|
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)
|
||||||
|
]
|
||||||
|
+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)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
void SGitSourceControlSettings::ConstructBasedOnEngineVersion( )
|
||||||
|
{
|
||||||
|
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
|
||||||
|
|
||||||
|
using Self = std::remove_pointer_t<decltype(this)>;
|
||||||
|
|
||||||
|
#define ROW_LEFT( PADDING_HEIGHT ) +SHorizontalBox::Slot() \
|
||||||
|
.VAlign(VAlign_Center) \
|
||||||
|
.HAlign(HAlign_Right) \
|
||||||
|
.FillWidth(1.0f) \
|
||||||
|
.Padding(FMargin(0.0f, 0.0f, 16.0f, PADDING_HEIGHT))
|
||||||
|
|
||||||
|
#define ROW_RIGHT( PADDING_HEIGHT ) +SHorizontalBox::Slot() \
|
||||||
|
.VAlign(VAlign_Center) \
|
||||||
|
.FillWidth(2.0f) \
|
||||||
|
.Padding(FMargin(0.0f, 0.0f, 0.0f, PADDING_HEIGHT))
|
||||||
|
|
||||||
|
#define TT_GitPath LOCTEXT("BinaryPathLabel_Tooltip", "Path to Git binary")
|
||||||
|
#define TT_RepoRoot LOCTEXT("RepositoryRootLabel_Tooltip", "Path to the root of the Git repository")
|
||||||
|
#define TT_UserName LOCTEXT("UserNameLabel_Tooltip", "Git Username fetched from local config")
|
||||||
|
#define TT_Email LOCTEXT("GitUserEmail_Tooltip", "Git E-mail fetched from local config")
|
||||||
|
#define TT_LFS LOCTEXT("UseGitLfsLocking_Tooltip", "Uses Git LFS 2 File Locking workflow (CheckOut and Commit/Push).")
|
||||||
|
|
||||||
|
ChildSlot
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
// Git Path
|
||||||
|
+SVerticalBox::Slot()
|
||||||
|
.AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
ROW_LEFT( 10.0f )
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(LOCTEXT("BinaryPathLabel", "Git Path"))
|
||||||
|
.ToolTipText( TT_GitPath )
|
||||||
|
]
|
||||||
|
ROW_RIGHT( 10.0f )
|
||||||
|
[
|
||||||
|
SNew(SFilePathPicker)
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
|
||||||
|
.BrowseButtonImage(FAppStyle::GetBrush("PropertyWindow.Button_Ellipsis"))
|
||||||
|
.BrowseButtonStyle(FAppStyle::Get(), "HoverHintOnly")
|
||||||
|
#else
|
||||||
|
.BrowseButtonImage(FEditorStyle::GetBrush("PropertyWindow.Button_Ellipsis"))
|
||||||
|
.BrowseButtonStyle(FEditorStyle::Get(), "HoverHintOnly")
|
||||||
|
#endif
|
||||||
|
.BrowseDirectory(FEditorDirectories::Get().GetLastDirectory(ELastDirectory::GENERIC_OPEN))
|
||||||
|
.BrowseTitle(LOCTEXT("BinaryPathBrowseTitle", "File picker..."))
|
||||||
|
.FilePath(this, &Self::GetBinaryPathString)
|
||||||
|
.FileTypeFilter(FileFilterText)
|
||||||
|
.OnPathPicked(this, &Self::OnBinaryPathPicked)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
// Repository Root
|
||||||
|
+SVerticalBox::Slot()
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
ROW_LEFT( 10.0f )
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(LOCTEXT("RepositoryRootLabel", "Root of the repository"))
|
||||||
|
.ToolTipText( TT_RepoRoot )
|
||||||
|
]
|
||||||
|
ROW_RIGHT( 10.0f )
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(this, &Self::GetPathToRepositoryRoot)
|
||||||
|
.ToolTipText( TT_RepoRoot )
|
||||||
|
]
|
||||||
|
]
|
||||||
|
// User Name
|
||||||
|
+SVerticalBox::Slot()
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
ROW_LEFT( 10.0f )
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(LOCTEXT("UserNameLabel", "User Name"))
|
||||||
|
.ToolTipText( TT_UserName )
|
||||||
|
]
|
||||||
|
ROW_RIGHT( 10.0f )
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(this, &Self::GetUserName)
|
||||||
|
.ToolTipText( TT_UserName )
|
||||||
|
]
|
||||||
|
]
|
||||||
|
// Email
|
||||||
|
+SVerticalBox::Slot()
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
ROW_LEFT( 10.0f )
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(LOCTEXT("EmailLabel", "E-mail"))
|
||||||
|
.ToolTipText( TT_Email )
|
||||||
|
]
|
||||||
|
ROW_RIGHT( 10.0f )
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(this, &Self::GetUserEmail )
|
||||||
|
.ToolTipText( TT_Email )
|
||||||
|
]
|
||||||
|
]
|
||||||
|
// LFS Config
|
||||||
|
+SVerticalBox::Slot()
|
||||||
|
.AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
ROW_LEFT( 10.0f )
|
||||||
|
[
|
||||||
|
SNew(SCheckBox)
|
||||||
|
.IsChecked(Self::IsUsingGitLfsLocking())
|
||||||
|
.OnCheckStateChanged(this, &Self::OnCheckedUseGitLfsLocking)
|
||||||
|
.IsEnabled(this, &Self::CanUseGitLfsLocking)
|
||||||
|
.Content()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(LOCTEXT("UseGitLfsLocking", "Uses Git LFS"))
|
||||||
|
.ToolTipText( TT_LFS )
|
||||||
|
]
|
||||||
|
]
|
||||||
|
ROW_RIGHT( 10.0f )
|
||||||
|
[
|
||||||
|
SNew(SEditableTextBox)
|
||||||
|
.Text(this, &Self::GetLfsUserName)
|
||||||
|
.OnTextCommitted(this, &Self::OnLfsUserNameCommited)
|
||||||
|
.IsEnabled(this, &Self::GetIsUsingGitLfsLocking)
|
||||||
|
.HintText(LOCTEXT("LfsUserName_Hint", "Username to lock files on the LFS server"))
|
||||||
|
]
|
||||||
|
]
|
||||||
|
// [Optional] Initial Git Commit
|
||||||
|
+SVerticalBox::Slot()
|
||||||
|
.AutoHeight()
|
||||||
|
.Padding(2.0f)
|
||||||
|
.VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
.ToolTipText(LOCTEXT("InitialGitCommit_Tooltip", "Make the initial Git commit"))
|
||||||
|
.Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository)
|
||||||
|
+ 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"))
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot()
|
||||||
|
.FillWidth(2.0f)
|
||||||
|
.Padding(2.0f)
|
||||||
|
[
|
||||||
|
SNew(SMultiLineEditableTextBox)
|
||||||
|
.Text(this, &SGitSourceControlSettings::GetInitialCommitMessage)
|
||||||
|
.OnTextCommitted(this, &SGitSourceControlSettings::OnInitialCommitMessageCommited)
|
||||||
|
.IsEnabled(this, &SGitSourceControlSettings::GetAutoInitialCommit)
|
||||||
|
.SelectAllTextWhenFocused(true)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
// [Optional] Initialize Project with Git
|
||||||
|
+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)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// TODO [RW] The UE5 GUI for the two optional initial git support functionalities has not been tested
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SGitSourceControlSettings::~SGitSourceControlSettings()
|
||||||
|
{
|
||||||
|
RemoveInProgressNotification();
|
||||||
|
}
|
||||||
|
|
||||||
|
FString SGitSourceControlSettings::GetBinaryPathString() const
|
||||||
|
{
|
||||||
|
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
return GitSourceControl.AccessSettings().GetBinaryPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SGitSourceControlSettings::OnBinaryPathPicked( const FString& PickedPath ) const
|
||||||
|
{
|
||||||
|
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
FString PickedFullPath = FPaths::ConvertRelativePathToFull(PickedPath);
|
||||||
|
const bool bChanged = GitSourceControl.AccessSettings().SetBinaryPath(PickedFullPath);
|
||||||
|
if(bChanged)
|
||||||
|
{
|
||||||
|
// 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 = FGitSourceControlModule::Get();
|
||||||
|
const FString& PathToRepositoryRoot = GitSourceControl.GetProvider().GetPathToRepositoryRoot();
|
||||||
|
return FText::FromString(PathToRepositoryRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
FText SGitSourceControlSettings::GetUserName() const
|
||||||
|
{
|
||||||
|
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
const FString& UserName = GitSourceControl.GetProvider().GetUserName();
|
||||||
|
return FText::FromString(UserName);
|
||||||
|
}
|
||||||
|
|
||||||
|
FText SGitSourceControlSettings::GetUserEmail() const
|
||||||
|
{
|
||||||
|
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
const FString& UserEmail = GitSourceControl.GetProvider().GetUserEmail();
|
||||||
|
return FText::FromString(UserEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
EVisibility SGitSourceControlSettings::MustInitializeGitRepository() const
|
||||||
|
{
|
||||||
|
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
const bool bGitAvailable = GitSourceControl.GetProvider().IsGitAvailable();
|
||||||
|
const bool bGitRepositoryFound = GitSourceControl.GetProvider().IsEnabled();
|
||||||
|
#if 0
|
||||||
|
return (bGitAvailable && !bGitRepositoryFound) ? EVisibility::Visible : EVisibility::Collapsed;
|
||||||
|
#else
|
||||||
|
return EVisibility::Collapsed;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SGitSourceControlSettings::CanInitializeGitRepository() const
|
||||||
|
{
|
||||||
|
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
const bool bGitAvailable = GitSourceControl.GetProvider().IsGitAvailable();
|
||||||
|
const bool bGitRepositoryFound = GitSourceControl.GetProvider().IsEnabled();
|
||||||
|
const FString& LfsUserName = GitSourceControl.AccessSettings().GetLfsUserName();
|
||||||
|
const bool bIsUsingGitLfsLocking = GitSourceControl.GetProvider().UsesCheckout();
|
||||||
|
const bool bGitLfsConfigOk = !bIsUsingGitLfsLocking || !LfsUserName.IsEmpty();
|
||||||
|
const bool bInitialCommitConfigOk = !bAutoInitialCommit || !InitialCommitMessage.IsEmpty();
|
||||||
|
#if 0
|
||||||
|
return (bGitAvailable && !bGitRepositoryFound && bGitLfsConfigOk && bInitialCommitConfigOk);
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SGitSourceControlSettings::CanUseGitLfsLocking() const
|
||||||
|
{
|
||||||
|
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
// TODO LFS SRombauts : check if .gitattributes file is present and if Content/ is already tracked!
|
||||||
|
const bool bGitAttributesCreated = true;
|
||||||
|
return (bAutoCreateGitAttributes || bGitAttributesCreated);
|
||||||
|
}
|
||||||
|
|
||||||
|
FReply SGitSourceControlSettings::OnClickedInitializeGitRepository()
|
||||||
|
{
|
||||||
|
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
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, FGitSourceControlModule::GetEmptyStringArray(), FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages);
|
||||||
|
// 1.b. Synchronous (very quick) "git remote add" operation: configure the URL of the default remote server 'origin' if specified
|
||||||
|
if(!RemoteUrl.IsEmpty())
|
||||||
|
{
|
||||||
|
TArray<FString> Parameters;
|
||||||
|
Parameters.Add(TEXT("add origin"));
|
||||||
|
Parameters.Add(RemoteUrl.ToString());
|
||||||
|
GitSourceControlUtils::RunCommand(TEXT("remote"), PathToGitBinary, PathToProjectDir, Parameters, FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the new repository status to enable connection (branch, user e-mail)
|
||||||
|
GitSourceControl.GetProvider().CheckGitAvailability();
|
||||||
|
if(GitSourceControl.GetProvider().IsAvailable())
|
||||||
|
{
|
||||||
|
// List of files to add to Revision Control (.uproject, Config/, Content/, Source/ files and .gitignore/.gitattributes if any)
|
||||||
|
TArray<FString> ProjectFiles;
|
||||||
|
ProjectFiles.Add(FPaths::ProjectContentDir());
|
||||||
|
ProjectFiles.Add(FPaths::ProjectConfigDir());
|
||||||
|
ProjectFiles.Add(FPaths::GetProjectFilePath());
|
||||||
|
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("install"), PathToGitBinary, PathToProjectDir, FGitSourceControlModule::GetEmptyStringArray(), FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages);
|
||||||
|
|
||||||
|
// 2.d. Create a ".gitattributes" file to enable Git LFS (Large File System) for the whole "Content/" subdir
|
||||||
|
const FString GitAttributesFilename = FPaths::Combine(FPaths::ProjectDir(), TEXT(".gitattributes"));
|
||||||
|
FString GitAttributesContent;
|
||||||
|
if (GitSourceControl.GetProvider().UsesCheckout())
|
||||||
|
{
|
||||||
|
// 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 Revision Control: launch an asynchronous MarkForAdd operation
|
||||||
|
LaunchMarkForAddOperation(ProjectFiles);
|
||||||
|
|
||||||
|
// 4. The CheckIn will follow, at completion of the MarkForAdd operation
|
||||||
|
FGitSourceControlProvider& Provider = FGitSourceControlModule::Get().GetProvider();
|
||||||
|
Provider.CheckRepositoryStatus();
|
||||||
|
}
|
||||||
|
return FReply::Handled();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch an asynchronous "MarkForAdd" operation and start an ongoing notification
|
||||||
|
void SGitSourceControlSettings::LaunchMarkForAddOperation(const TArray<FString>& InFiles)
|
||||||
|
{
|
||||||
|
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
TSharedRef<FMarkForAdd, ESPMode::ThreadSafe> MarkForAddOperation = ISourceControlOperation::Create<FMarkForAdd>();
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
ECommandResult::Type Result = GitSourceControl.GetProvider().Execute(MarkForAddOperation, FSourceControlChangelistPtr(), InFiles, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateSP(this, &SGitSourceControlSettings::OnSourceControlOperationComplete));
|
||||||
|
#else
|
||||||
|
ECommandResult::Type Result = GitSourceControl.GetProvider().Execute(MarkForAddOperation, InFiles, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateSP(this, &SGitSourceControlSettings::OnSourceControlOperationComplete));
|
||||||
|
#endif
|
||||||
|
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 = FGitSourceControlModule::Get();
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5
|
||||||
|
ECommandResult::Type Result = GitSourceControl.GetProvider().Execute(CheckInOperation, FSourceControlChangelistPtr(), FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateSP(this, &SGitSourceControlSettings::OnSourceControlOperationComplete));
|
||||||
|
#else
|
||||||
|
ECommandResult::Type Result = GitSourceControl.GetProvider().Execute(CheckInOperation, FGitSourceControlModule::GetEmptyStringArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateSP(this, &SGitSourceControlSettings::OnSourceControlOperationComplete));
|
||||||
|
#endif
|
||||||
|
if (Result == ECommandResult::Succeeded)
|
||||||
|
{
|
||||||
|
DisplayInProgressNotification(CheckInOperation);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DisplayFailureNotification(CheckInOperation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delegate called when a Revision control operation has completed: launch the next one and manage notifications
|
||||||
|
void SGitSourceControlSettings::OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
|
||||||
|
Info.Image = FAppStyle::GetBrush(TEXT("NotificationList.SuccessImage"));
|
||||||
|
#else
|
||||||
|
Info.Image = FEditorStyle::GetBrush(TEXT("NotificationList.SuccessImage"));
|
||||||
|
#endif
|
||||||
|
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 = FGitSourceControlModule::Get();
|
||||||
|
GitSourceControl.AccessSettings().SetUsingGitLfsLocking(NewCheckedState == ECheckBoxState::Checked);
|
||||||
|
GitSourceControl.AccessSettings().SaveSettings();
|
||||||
|
GitSourceControl.GetProvider().UpdateSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SGitSourceControlSettings::GetIsUsingGitLfsLocking() const
|
||||||
|
{
|
||||||
|
const FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
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 = FGitSourceControlModule::Get();
|
||||||
|
GitSourceControl.AccessSettings().SetLfsUserName(InText.ToString());
|
||||||
|
GitSourceControl.AccessSettings().SaveSettings();
|
||||||
|
GitSourceControl.GetProvider().UpdateSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
FText SGitSourceControlSettings::GetLfsUserName() const
|
||||||
|
{
|
||||||
|
FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get();
|
||||||
|
const FString LFSUserName = GitSourceControl.AccessSettings().GetLfsUserName();
|
||||||
|
if (LFSUserName.IsEmpty())
|
||||||
|
{
|
||||||
|
const FText& UserName = GetUserName();
|
||||||
|
GitSourceControl.AccessSettings().SetLfsUserName(UserName.ToString());
|
||||||
|
GitSourceControl.AccessSettings().SaveSettings();
|
||||||
|
GitSourceControl.GetProvider().UpdateSettings();
|
||||||
|
return UserName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return FText::FromString(LFSUserName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SGitSourceControlSettings::OnCheckedInitialCommit(ECheckBoxState NewCheckedState)
|
||||||
|
{
|
||||||
|
bAutoInitialCommit = (NewCheckedState == ECheckBoxState::Checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SGitSourceControlSettings::GetAutoInitialCommit() const
|
||||||
|
{
|
||||||
|
return bAutoInitialCommit;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SGitSourceControlSettings::OnInitialCommitMessageCommited(const FText& InText, ETextCommit::Type InCommitType)
|
||||||
|
{
|
||||||
|
InitialCommitMessage = InText;
|
||||||
|
}
|
||||||
|
|
||||||
|
FText SGitSourceControlSettings::GetInitialCommitMessage() const
|
||||||
|
{
|
||||||
|
return InitialCommitMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SGitSourceControlSettings::OnRemoteUrlCommited(const FText& InText, ETextCommit::Type InCommitType)
|
||||||
|
{
|
||||||
|
RemoteUrl = InText;
|
||||||
|
}
|
||||||
|
|
||||||
|
FText SGitSourceControlSettings::GetRemoteUrl() const
|
||||||
|
{
|
||||||
|
return RemoteUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
@ -0,0 +1,100 @@
|
|||||||
|
// 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 "Widgets/SCompoundWidget.h"
|
||||||
|
#include "ISourceControlProvider.h"
|
||||||
|
#include "Runtime/Launch/Resources/Version.h"
|
||||||
|
|
||||||
|
class SNotificationItem;
|
||||||
|
#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 2
|
||||||
|
namespace ETextCommit { enum Type : int; }
|
||||||
|
#else
|
||||||
|
namespace ETextCommit { enum Type; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum class ECheckBoxState : uint8;
|
||||||
|
|
||||||
|
class SGitSourceControlSettings : public SCompoundWidget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
|
||||||
|
SLATE_BEGIN_ARGS(SGitSourceControlSettings) {}
|
||||||
|
|
||||||
|
SLATE_END_ARGS()
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
void Construct(const FArguments& InArgs);
|
||||||
|
|
||||||
|
~SGitSourceControlSettings();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ConstructBasedOnEngineVersion( );
|
||||||
|
|
||||||
|
/** 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 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 revision control operation has completed */
|
||||||
|
void OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult);
|
||||||
|
|
||||||
|
/** Asynchronous operation progress notifications */
|
||||||
|
TWeakPtr<SNotificationItem> OperationInProgressNotification;
|
||||||
|
|
||||||
|
void DisplayInProgressNotification(const FSourceControlOperationRef& InOperation);
|
||||||
|
void RemoveInProgressNotification();
|
||||||
|
void DisplaySuccessNotification(const FSourceControlOperationRef& InOperation);
|
||||||
|
void DisplayFailureNotification(const FSourceControlOperationRef& InOperation);
|
||||||
|
};
|
2
Plugins/UEGitPlugin/_config.yml
Normal file
2
Plugins/UEGitPlugin/_config.yml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
show_downloads: true
|
||||||
|
theme: jekyll-theme-slate
|
BIN
Plugins/UEGitPlugin/git-lfs
Normal file
BIN
Plugins/UEGitPlugin/git-lfs
Normal file
Binary file not shown.
BIN
Plugins/UEGitPlugin/git-lfs-mac-amd64
Normal file
BIN
Plugins/UEGitPlugin/git-lfs-mac-amd64
Normal file
Binary file not shown.
BIN
Plugins/UEGitPlugin/git-lfs-mac-arm64
Normal file
BIN
Plugins/UEGitPlugin/git-lfs-mac-arm64
Normal file
Binary file not shown.
BIN
Plugins/UEGitPlugin/git-lfs.exe
Normal file
BIN
Plugins/UEGitPlugin/git-lfs.exe
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user