├── .gitbugtraq
├── .gitignore
├── GitSourceControl.uplugin
├── LICENSE.txt
├── README.md
├── Resources
└── Icon128.png
├── Screenshots
├── FileHistory.png
├── Icons
│ ├── Added.png
│ ├── Modified.png
│ ├── New.png
│ ├── Renamed.png
│ └── Unchanged.png
├── SourceControlLogin_Init.png
├── SourceControlMenu.png
├── SourceControlStatusTooltip.png
└── SubmitFiles.png
├── Source
└── GitSourceControl
│ ├── GitSourceControl.Build.cs
│ └── Private
│ ├── GitSourceControlCommand.cpp
│ ├── GitSourceControlCommand.h
│ ├── GitSourceControlConsole.cpp
│ ├── GitSourceControlConsole.h
│ ├── GitSourceControlMenu.cpp
│ ├── GitSourceControlMenu.h
│ ├── GitSourceControlModule.cpp
│ ├── GitSourceControlModule.h
│ ├── GitSourceControlOperations.cpp
│ ├── GitSourceControlOperations.h
│ ├── GitSourceControlPrivatePCH.h
│ ├── GitSourceControlProvider.cpp
│ ├── GitSourceControlProvider.h
│ ├── GitSourceControlRevision.cpp
│ ├── GitSourceControlRevision.h
│ ├── GitSourceControlSettings.cpp
│ ├── GitSourceControlSettings.h
│ ├── GitSourceControlState.cpp
│ ├── GitSourceControlState.h
│ ├── GitSourceControlUtils.cpp
│ ├── GitSourceControlUtils.h
│ ├── IGitSourceControlWorker.h
│ ├── SGitSourceControlSettings.cpp
│ └── SGitSourceControlSettings.h
└── _config.yml
/.gitbugtraq:
--------------------------------------------------------------------------------
1 | # .gitbugtraq for Git GUIs (SmartGit/TortoiseGit) to show links to the Github issue tracker.
2 | # Instead of the repository root directory, it could be added as an additional section to $GIT_DIR/config.
3 | # (note that '\' need to be escaped).
4 | [bugtraq]
5 | url = https://github.com/SRombauts/UE4GitPlugin/issues/%BUGID%
6 | loglinkregex = "#\\d+"
7 | logregex = \\d+
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /Binaries
2 | /Intermediate
3 |
--------------------------------------------------------------------------------
/GitSourceControl.uplugin:
--------------------------------------------------------------------------------
1 | {
2 | "FileVersion" : 3,
3 | "Version" : 52,
4 | "VersionName" : "2.52",
5 | "FriendlyName" : "Git LFS 2",
6 | "Description" : "Git source control management (dev)",
7 | "Category" : "Source Control",
8 | "CreatedBy" : "SRombauts",
9 | "CreatedByURL" : "https://srombauts.github.io",
10 | "DocsURL" : "",
11 | "MarketplaceURL" : "",
12 | "SupportURL" : "",
13 | "EnabledByDefault" : true,
14 | "CanContainContent" : false,
15 | "IsBetaVersion" : true,
16 | "Installed" : false,
17 | "Modules" :
18 | [
19 | {
20 | "Name" : "GitSourceControl",
21 | "Type" : "Editor",
22 | "LoadingPhase" : "Default"
23 | }
24 | ]
25 | }
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is furnished
10 | to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
19 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
20 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Unreal Engine 5 Git Source Control Plugin
2 | -----------------------------------------
3 |
4 | [](https://github.com/SRombauts/UE4GitPlugin/releases)
5 | [](https://github.com/SRombauts/UE4GitPlugin/issues)
6 | [](https://gitter.im/SRombauts/UE4GitPlugin)
7 | UEGitPlugin is a simple Git Source Control Plugin for **Unreal Engine 5.0, 5.1, 5.2 and 4.27**.
8 |
9 | Developed and contributed by Sébastien Rombauts 2014-2023 (sebastien.rombauts@gmail.com)
10 |
11 | 1. First version of the plugin has been **integrated by default in UE4.7 in "beta version"**.
12 | 2. This is a developement fork named "**Git LFS 2**" adding File Locks supported by Github.
13 | 3. ProjectBorealis has been busy fixing and improving this plugin to make it work robustly with LFS Locks.
14 | **See [ProjectBorealis GitPlugin v3](https://github.com/ProjectBorealis/UEGitPlugin)**
15 |
16 | ### Instructions
17 |
18 | You need to install it into your Project **Plugins/** folder, and it will overwrite (replace) the default "Git (beta version)" Source Control Provider with the "Git LFS 2" plugin.
19 |
20 | Have a look at the [Git Plugin Tutorial on the Wiki](https://wiki.unrealengine.com/Git_source_control_%28Tutorial%29). ([alternate link](https://michaeljcole.github.io/wiki.unrealengine.com/Git_source_control_%28Tutorial%29/))
21 |
22 | Written and contributed by Sebastien Rombauts (sebastien.rombauts@gmail.com)
23 |
24 | Source Control Login window to create a new workspace/a new repository:
25 | 
26 |
27 | Source Control status tooltip, when hovering the Source Control icon in toolbar:
28 | 
29 |
30 | Source Control top Menu, extended with a few commands specific to Git:
31 | 
32 |
33 | Submit Files to Source Control window, to commit assets:
34 | 
35 |
36 | File History window, to see the changelog of an asset:
37 | 
38 |
39 | Visual Diffing of two revisions of a Blueprint:
40 |
41 |
42 | Merge conflict of a Blueprint:
43 |
44 |
45 | Status Icons:
46 |
47 | 
48 | 
49 | 
50 | 
51 | 
52 |
53 | ### Supported features
54 | - initialize a new Git local repository ('git init') to manage your UE4 Game Project
55 | - can also create an appropriate .gitignore file as part of initialization
56 | - can also create a .gitattributes file to enable Git LFS (Large File System) as part of initialization
57 | - can also enable Git LFS 2.x File Locks as part of initialization
58 | - can also make the initial commit, with custom multi-line message
59 | - display status icons to show modified/added/deleted/untracked files, not at head and conflicted
60 | - show history of a file
61 | - visual diff of a blueprint against depot or between previous versions of a file
62 | - revert modifications of a file (works best with "Content Hot-Reload" experimental option of UE4.15, by default since 4.16)
63 | - add, delete, rename a file
64 | - checkin/commit a file (cannot handle atomically more than 50 files)
65 | - migrate an asset between two projects if both are using Git
66 | - solve a merge conflict on a blueprint
67 | - show current branch name in status text
68 | - Configure remote origin URL ('git remote add origin url')
69 | - Sync to Pull (rebase) the current branch if there is no local modified files
70 | - Push the current branch
71 | - Git LFS (Github, Gitlab, Bitbucket), git-annex, git-fat and git-media are working with Git 2.10+
72 | - Git LFS 2 File Locks
73 | - Git console command for the Editor
74 | - Windows, Mac and Linux
75 |
76 | ### What *cannot* be done presently
77 | - Branch/Merge are not in the current Editor workflow
78 | - Amend a commit is not in the current Editor workflow
79 | - Revert All (using either "Stash" or "reset --hard")
80 | - Configure user name & email ('git config user.name' & git config user.email')
81 | - Authentication is not managed if needed for Sync (Pull)
82 |
83 | ### Known issues
84 | - #34 "outside repository" fatal error
85 | - #37 Rebase workflow: conflicts not detected!
86 | - #41 UE-44637: Deleting an asset is unsuccessful if the asset is marked for add (since UE4.13)
87 | - #46 Merge Conflicts - Accept Target - causes engine to crash bug
88 | - #47 Git LFS conflict resolution not working
89 | - #49 Git LFS 2: False error in logs after a successful push
90 | - #51 Git LFS 2: cannot revert a modified/unchecked-out asset
91 | - #53 Git LFS 2: document the configuration and workflow
92 | - #54 Poor performances of 'lfs locks' on Windows command line
93 | - #55 Git LFS 2: Unlocking a renamed asset
94 |
95 | - missing localisation for git specific messages
96 | - displaying states of 'Engine' assets (also needs management of 'out of tree' files)
97 | - renaming a Blueprint in Editor leaves a redirector file, AND modify too much the asset to enable git to track its history through renaming
98 |
99 | ### Getting started
100 |
101 | Quick demo of the Git Plugin on Unreal Engine 4.12 (preview)
102 | [](https://youtu.be/rRhPl9vL58Q)
103 |
104 | #### Install Git
105 |
106 | Under Windows 64bits, you should install the standard standalone Git for Windows
107 | (now comming with Git LFS 2 with File Locking) with default parameters,
108 | usually in "C:\Program Files\Git\bin\git.exe".
109 |
110 | Then you have to configure your name and e-mail that will appear in each of your commits:
111 |
112 | ```
113 | git config --global user.name "Sébastien Rombauts"
114 | git config --global user.email sebastien.rombauts@gmail.com
115 | ```
116 |
117 | #### Install this Git Plugin (dev) into your Game Project
118 |
119 | Unreal Engine comes with a stable version of this plugin, so no need to install it.
120 |
121 | This alternate "Git development plugin" needs to be installed into a subfolder or your Game Project "Plugins" directory
122 | (that is, you cannot install it into the Engine Plugins directory):
123 |
124 | ```
125 | /Plugins
126 | ```
127 |
128 | You will obviously only be able to use the plugin within this project.
129 |
130 | See also the [Plugins official Documentation](https://docs.unrealengine.com/latest/INT/Programming/Plugins/index.html)
131 |
132 | #### Activate Git Source Control for your Game Project
133 |
134 | Load your Game Project in Unreal Engine, then open:
135 |
136 | ```
137 | File->Connect To Source Control... -> Git
138 | ```
139 |
140 | ##### Project already managed by Git
141 |
142 | If your project is already under Git (it contains a ".git" subfolder), just click on "Accept Settings". This connect the Editor to your local Git repository ("Depot").
143 |
144 | ##### Project not already under Git
145 |
146 | Otherwise, the Git Plugin is able to create (initialize) a new local Git Repository with your project Assets and Sources files:
147 |
148 |
149 |
150 | Click "Initialize project with Git" that will add all relevant files to source control and make the initial commit with the customizable message.
151 | When everything is done, click on "Accept Settings".
152 |
153 | #### Using the Git Source Control Provider in the Unreal Engine Editor
154 |
155 | The plugin mostly interacts with you local Git repository ("Depot"), not much with the remote server (usually "origin").
156 |
157 | It displays Git status icons on top of assets in the Asset Browser:
158 | - No icon means that the file is under source control and unchanged since last commit, or ignored.
159 | - A red mark is for "modified" assets, that is the one that needs to be committed (so not the same as "Check-out" in Perforce/SVN/Plastic SCM).
160 | - A red cross is for "added" assets, that also needs to be committed
161 | - A blue lightning means "renamed".
162 | - A yellow exclamation point is for files in conflict after a merge, or is not at head (latest revision on the current remote branch).
163 | - A yellow question mark is for files not in source control.
164 |
165 | TODO:
166 | - specifics of rename and redirectors, and "Fix Up Redirector in Folder" command
167 | - history / visual diff
168 | - CheckIn = Commit
169 | - CheckOut = Commit+Push+unlock (when using LFS 2)
170 |
171 | See also the [Source Control official Documentation](https://docs.unrealengine.com/latest/INT/Engine/UI/SourceControl/index.html)
172 |
173 | ### License
174 |
175 | Copyright (c) 2014-2020 Sébastien Rombauts (sebastien.rombauts@gmail.com)
176 |
177 | Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
178 | or copy at http://opensource.org/licenses/MIT)
179 |
180 | ## How to contribute
181 | ### GitHub website
182 | The most efficient way to help and contribute to this wrapper project is to
183 | use the tools provided by GitHub:
184 | - please fill bug reports and feature requests here: https://github.com/SRombauts/UE4GitPlugin/issues
185 | - fork the repository, make some small changes and submit them with independent pull-requests
186 |
187 | ### Contact
188 | - You can use the Unreal Engine forums.
189 | - You can also email me directly, I will answer any questions and requests.
190 |
191 | ### Coding Style Guidelines
192 | The source code follow the Unreal Engine official [Coding Standard](https://docs.unrealengine.com/latest/INT/Programming/Development/CodingStandard/index.html):
193 | - CamelCase naming convention, with a prefix letter to differentiate classes ('F'), interfaces ('I'), templates ('T')
194 | - files (.cpp/.h) are named like the class they contains
195 | - Doxygen comments, documentation is located with declaration, on headers
196 | - Use portable common features of C++11 like nullptr, auto, range based for, override keyword
197 | - Braces on their own line
198 | - Tabs to indent code, with a width of 4 characters
199 |
200 | ## See also
201 |
202 | - [Git Source Control Tutorial on the Wikis](https://wiki.unrealengine.com/Git_source_control_(Tutorial))
203 | - [UE4 Git Plugin website](http://srombauts.github.com/UE4GitPlugin)
204 |
205 | - [ue4-hg-plugin for Mercurial (and bigfiles)](https://github.com/enlight/ue4-hg-plugin)
206 |
--------------------------------------------------------------------------------
/Resources/Icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SRombauts/UEGitPlugin/ee6af92974d8a0658af78938f14675f080bf14d0/Resources/Icon128.png
--------------------------------------------------------------------------------
/Screenshots/FileHistory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SRombauts/UEGitPlugin/ee6af92974d8a0658af78938f14675f080bf14d0/Screenshots/FileHistory.png
--------------------------------------------------------------------------------
/Screenshots/Icons/Added.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SRombauts/UEGitPlugin/ee6af92974d8a0658af78938f14675f080bf14d0/Screenshots/Icons/Added.png
--------------------------------------------------------------------------------
/Screenshots/Icons/Modified.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SRombauts/UEGitPlugin/ee6af92974d8a0658af78938f14675f080bf14d0/Screenshots/Icons/Modified.png
--------------------------------------------------------------------------------
/Screenshots/Icons/New.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SRombauts/UEGitPlugin/ee6af92974d8a0658af78938f14675f080bf14d0/Screenshots/Icons/New.png
--------------------------------------------------------------------------------
/Screenshots/Icons/Renamed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SRombauts/UEGitPlugin/ee6af92974d8a0658af78938f14675f080bf14d0/Screenshots/Icons/Renamed.png
--------------------------------------------------------------------------------
/Screenshots/Icons/Unchanged.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SRombauts/UEGitPlugin/ee6af92974d8a0658af78938f14675f080bf14d0/Screenshots/Icons/Unchanged.png
--------------------------------------------------------------------------------
/Screenshots/SourceControlLogin_Init.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SRombauts/UEGitPlugin/ee6af92974d8a0658af78938f14675f080bf14d0/Screenshots/SourceControlLogin_Init.png
--------------------------------------------------------------------------------
/Screenshots/SourceControlMenu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SRombauts/UEGitPlugin/ee6af92974d8a0658af78938f14675f080bf14d0/Screenshots/SourceControlMenu.png
--------------------------------------------------------------------------------
/Screenshots/SourceControlStatusTooltip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SRombauts/UEGitPlugin/ee6af92974d8a0658af78938f14675f080bf14d0/Screenshots/SourceControlStatusTooltip.png
--------------------------------------------------------------------------------
/Screenshots/SubmitFiles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SRombauts/UEGitPlugin/ee6af92974d8a0658af78938f14675f080bf14d0/Screenshots/SubmitFiles.png
--------------------------------------------------------------------------------
/Source/GitSourceControl/GitSourceControl.Build.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 | //
3 | // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
4 | // or copy at http://opensource.org/licenses/MIT)
5 |
6 | using UnrealBuildTool;
7 |
8 | public class GitSourceControl : ModuleRules
9 | {
10 | public GitSourceControl(ReadOnlyTargetRules Target) : base(Target)
11 | {
12 | PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
13 | PrivatePCHHeaderFile = "Private/GitSourceControlPrivatePCH.h";
14 |
15 | PrivateDependencyModuleNames.AddRange(
16 | new[] {
17 | "Core",
18 | "CoreUObject",
19 | "Slate",
20 | "SlateCore",
21 | "InputCore",
22 | "DesktopWidgets",
23 | "EditorStyle",
24 | "UnrealEd",
25 | "SourceControl",
26 | "Projects",
27 | }
28 | );
29 |
30 | if (Target.Version.MajorVersion == 5)
31 | {
32 | PrivateDependencyModuleNames.Add("ToolMenus");
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlCommand.cpp:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 | //
3 | // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
4 | // or copy at http://opensource.org/licenses/MIT)
5 |
6 | #include "GitSourceControlCommand.h"
7 |
8 | #include "Modules/ModuleManager.h"
9 | #include "GitSourceControlModule.h"
10 |
11 | FGitSourceControlCommand::FGitSourceControlCommand(const TSharedRef& InOperation, const TSharedRef& InWorker, const FSourceControlOperationComplete& InOperationCompleteDelegate)
12 | : Operation(InOperation)
13 | , Worker(InWorker)
14 | , OperationCompleteDelegate(InOperationCompleteDelegate)
15 | , bExecuteProcessed(0)
16 | , bCommandSuccessful(false)
17 | , bConnectionDropped(false)
18 | , bAutoDelete(true)
19 | , Concurrency(EConcurrency::Synchronous)
20 | {
21 | // grab the providers settings here, so we don't access them once the worker thread is launched
22 | check(IsInGameThread());
23 | const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked( "GitSourceControl" );
24 | PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
25 | bUsingGitLfsLocking = GitSourceControl.AccessSettings().IsUsingGitLfsLocking();
26 | PathToRepositoryRoot = GitSourceControl.GetProvider().GetPathToRepositoryRoot();
27 | }
28 |
29 | bool FGitSourceControlCommand::DoWork()
30 | {
31 | bCommandSuccessful = Worker->Execute(*this);
32 | FPlatformAtomics::InterlockedExchange(&bExecuteProcessed, 1);
33 |
34 | return bCommandSuccessful;
35 | }
36 |
37 | void FGitSourceControlCommand::Abandon()
38 | {
39 | FPlatformAtomics::InterlockedExchange(&bExecuteProcessed, 1);
40 | }
41 |
42 | void FGitSourceControlCommand::DoThreadedWork()
43 | {
44 | Concurrency = EConcurrency::Asynchronous;
45 | DoWork();
46 | }
47 |
48 | ECommandResult::Type FGitSourceControlCommand::ReturnResults()
49 | {
50 | // Save any messages that have accumulated
51 | for (FString& String : InfoMessages)
52 | {
53 | Operation->AddInfoMessge(FText::FromString(String));
54 | }
55 | for (FString& String : ErrorMessages)
56 | {
57 | Operation->AddErrorMessge(FText::FromString(String));
58 | }
59 |
60 | // run the completion delegate if we have one bound
61 | ECommandResult::Type Result = bCommandSuccessful ? ECommandResult::Succeeded : ECommandResult::Failed;
62 | OperationCompleteDelegate.ExecuteIfBound(Operation, Result);
63 |
64 | return Result;
65 | }
66 |
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlCommand.h:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 | //
3 | // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
4 | // or copy at http://opensource.org/licenses/MIT)
5 |
6 | #pragma once
7 |
8 | #include "CoreMinimal.h"
9 | #include "ISourceControlProvider.h"
10 | #include "Misc/IQueuedWork.h"
11 |
12 | /**
13 | * Used to execute Git commands multi-threaded.
14 | */
15 | class FGitSourceControlCommand : public IQueuedWork
16 | {
17 | public:
18 |
19 | FGitSourceControlCommand(const TSharedRef& InOperation, const TSharedRef& InWorker, const FSourceControlOperationComplete& InOperationCompleteDelegate = FSourceControlOperationComplete() );
20 |
21 | /**
22 | * This is where the real thread work is done. All work that is done for
23 | * this queued object should be done from within the call to this function.
24 | */
25 | bool DoWork();
26 |
27 | /**
28 | * Tells the queued work that it is being abandoned so that it can do
29 | * per object clean up as needed. This will only be called if it is being
30 | * abandoned before completion. NOTE: This requires the object to delete
31 | * itself using whatever heap it was allocated in.
32 | */
33 | virtual void Abandon() override;
34 |
35 | /**
36 | * This method is also used to tell the object to cleanup but not before
37 | * the object has finished it's work.
38 | */
39 | virtual void DoThreadedWork() override;
40 |
41 | /** Save any results and call any registered callbacks. */
42 | ECommandResult::Type ReturnResults();
43 |
44 | public:
45 | /** Path to the Git binary */
46 | FString PathToGitBinary;
47 |
48 | /** Path to the root of the Git repository: can be the ProjectDir itself, or any parent directory (found by the "Connect" operation) */
49 | FString PathToRepositoryRoot;
50 |
51 | /** Tell if using the Git LFS file Locking workflow */
52 | bool bUsingGitLfsLocking;
53 |
54 | /** Operation we want to perform - contains outward-facing parameters & results */
55 | TSharedRef Operation;
56 |
57 | /** The object that will actually do the work */
58 | TSharedRef Worker;
59 |
60 | /** Delegate to notify when this operation completes */
61 | FSourceControlOperationComplete OperationCompleteDelegate;
62 |
63 | /**If true, this command has been processed by the source control thread*/
64 | volatile int32 bExecuteProcessed;
65 |
66 | /**If true, the source control command succeeded*/
67 | bool bCommandSuccessful;
68 |
69 | /** TODO LFS If true, the source control connection was dropped while this command was being executed*/
70 | bool bConnectionDropped;
71 |
72 | /** Current Commit full SHA1 */
73 | FString CommitId;
74 |
75 | /** Current Commit description's Summary */
76 | FString CommitSummary;
77 |
78 | /** If true, this command will be automatically cleaned up in Tick() */
79 | bool bAutoDelete;
80 |
81 | /** Whether we are running multi-treaded or not*/
82 | EConcurrency::Type Concurrency;
83 |
84 | /** Files to perform this operation on */
85 | TArray Files;
86 |
87 | /**Info and/or warning message storage*/
88 | TArray InfoMessages;
89 |
90 | /**Potential error message storage*/
91 | TArray ErrorMessages;
92 | };
93 |
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlConsole.cpp:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 |
3 | #include "GitSourceControlConsole.h"
4 |
5 | #include "ISourceControlModule.h"
6 |
7 | #include "GitSourceControlModule.h"
8 | #include "GitSourceControlUtils.h"
9 |
10 | void FGitSourceControlConsole::Register()
11 | {
12 | if (!GitConsoleCommand.IsValid())
13 | {
14 | GitConsoleCommand = MakeUnique(
15 | TEXT("git"),
16 | TEXT("Git Command Line Interface.\n")
17 | TEXT("Run any 'git' command directly from the Unreal Editor Console.\n")
18 | TEXT("Type 'git help' to get a list of commands."),
19 | FConsoleCommandWithArgsDelegate::CreateRaw(this, &FGitSourceControlConsole::ExecuteGitConsoleCommand)
20 | );
21 | }
22 | }
23 |
24 | void FGitSourceControlConsole::Unregister()
25 | {
26 | GitConsoleCommand.Reset();
27 | }
28 |
29 | void FGitSourceControlConsole::ExecuteGitConsoleCommand(const TArray& a_args)
30 | {
31 | FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl");
32 | const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
33 | const FString& RepositoryRoot = GitSourceControl.GetProvider().GetPathToRepositoryRoot();
34 |
35 | // The first argument is the command to send to git, the following ones are forwarded as parameters for the command
36 | TArray Parameters = a_args;
37 | FString Command;
38 | if (a_args.Num() > 0)
39 | {
40 | Command = a_args[0];
41 | Parameters.RemoveAt(0);
42 | }
43 | else
44 | {
45 | // If no command is provided, use "help" to emulate the behavior of the git CLI
46 | Command = TEXT("help");
47 | }
48 |
49 | FString Results;
50 | FString Errors;
51 | GitSourceControlUtils::RunCommandInternalRaw(Command, PathToGitBinary, RepositoryRoot, Parameters, TArray(), Results, Errors);
52 |
53 | UE_LOG(LogSourceControl, Log, TEXT("Output:\n%s"), *Results);
54 | }
55 |
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlConsole.h:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 |
3 | #pragma once
4 |
5 | #include "CoreMinimal.h"
6 | #include "HAL/IConsoleManager.h"
7 |
8 | /**
9 | * Editor only console commands.
10 | *
11 | * Such commands can be executed from the editor output log window, but also from command line arguments,
12 | * from Editor Blueprints utilities, or from C++ Code using. eg. GEngine->Exec("git status Content/");
13 | */
14 | class FGitSourceControlConsole
15 | {
16 | public:
17 | void Register();
18 | void Unregister();
19 |
20 | private:
21 | // Git Command Line Interface: Run 'git' commands directly from the Unreal Editor Console.
22 | void ExecuteGitConsoleCommand(const TArray& a_args);
23 |
24 | /** Console command for interacting with 'git' CLI directly */
25 | TUniquePtr GitConsoleCommand;
26 | };
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlMenu.cpp:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 | //
3 | // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
4 | // or copy at http://opensource.org/licenses/MIT)
5 |
6 | #include "GitSourceControlMenu.h"
7 |
8 | #include "GitSourceControlModule.h"
9 | #include "GitSourceControlProvider.h"
10 | #include "GitSourceControlOperations.h"
11 | #include "GitSourceControlUtils.h"
12 |
13 | #include "ISourceControlModule.h"
14 | #include "ISourceControlOperation.h"
15 | #include "SourceControlOperations.h"
16 |
17 | #include "LevelEditor.h"
18 | #include "Widgets/Notifications/SNotificationList.h"
19 | #include "Framework/Notifications/NotificationManager.h"
20 | #include "Framework/MultiBox/MultiBoxBuilder.h"
21 | #include "Misc/MessageDialog.h"
22 | #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
23 | #include "Styling/AppStyle.h"
24 | #else
25 | #include "EditorStyleSet.h"
26 | #endif
27 |
28 | #include "PackageTools.h"
29 | #include "FileHelpers.h"
30 |
31 | #include "Logging/MessageLog.h"
32 | #include "UObject/Linker.h"
33 |
34 | #if ENGINE_MAJOR_VERSION == 5
35 | #include "ToolMenus.h"
36 | #include "ToolMenuContext.h"
37 | #include "ToolMenuMisc.h"
38 | #endif
39 |
40 | #define LOCTEXT_NAMESPACE "GitSourceControl"
41 |
42 | void FGitSourceControlMenu::Register()
43 | {
44 | // Register the extension with the level editor
45 |
46 | #if ENGINE_MAJOR_VERSION == 5
47 | FToolMenuOwnerScoped SourceControlMenuOwner("GitSourceControlMenu");
48 | if (UToolMenus* ToolMenus = UToolMenus::Get())
49 | {
50 | UToolMenu* SourceControlMenu = ToolMenus->ExtendMenu("StatusBar.ToolBar.SourceControl");
51 | FToolMenuSection& Section = SourceControlMenu->AddSection("GitSourceControlActions", LOCTEXT("GitSourceControlMenuHeadingActions", "Git"), FToolMenuInsert(NAME_None, EToolMenuInsertType::First));
52 |
53 | AddMenuExtension(Section);
54 | }
55 | #else
56 | FLevelEditorModule* LevelEditorModule = FModuleManager::GetModulePtr("LevelEditor");
57 | if (LevelEditorModule)
58 | {
59 | FLevelEditorModule::FLevelEditorMenuExtender ViewMenuExtender = FLevelEditorModule::FLevelEditorMenuExtender::CreateRaw(this, &FGitSourceControlMenu::OnExtendLevelEditorViewMenu);
60 | auto& MenuExtenders = LevelEditorModule->GetAllLevelEditorToolbarSourceControlMenuExtenders();
61 | MenuExtenders.Add(ViewMenuExtender);
62 | ViewMenuExtenderHandle = MenuExtenders.Last().GetHandle();
63 | }
64 | #endif
65 | }
66 |
67 | void FGitSourceControlMenu::Unregister()
68 | {
69 | // Unregister the level editor extensions
70 | #if ENGINE_MAJOR_VERSION == 5
71 | if (UToolMenus* ToolMenus = UToolMenus::Get())
72 | {
73 | UToolMenus::Get()->UnregisterOwnerByName("GitSourceControlMenu");
74 | }
75 | #else
76 | FLevelEditorModule* LevelEditorModule = FModuleManager::GetModulePtr("LevelEditor");
77 | if (LevelEditorModule)
78 | {
79 | LevelEditorModule->GetAllLevelEditorToolbarSourceControlMenuExtenders().RemoveAll([=](const FLevelEditorModule::FLevelEditorMenuExtender& Extender) { return Extender.GetHandle() == ViewMenuExtenderHandle; });
80 | }
81 | #endif
82 | }
83 |
84 | bool FGitSourceControlMenu::HaveRemoteUrl() const
85 | {
86 | const FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl");
87 | const FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
88 | return !Provider.GetRemoteUrl().IsEmpty();
89 | }
90 |
91 | /// Prompt to save or discard all packages
92 | bool FGitSourceControlMenu::SaveDirtyPackages()
93 | {
94 | const bool bPromptUserToSave = true;
95 | const bool bSaveMapPackages = true;
96 | const bool bSaveContentPackages = true;
97 | const bool bFastSave = false;
98 | const bool bNotifyNoPackagesSaved = false;
99 | const bool bCanBeDeclined = true; // If the user clicks "don't save" this will continue and lose their changes
100 | bool bHadPackagesToSave = false;
101 |
102 | bool bSaved = FEditorFileUtils::SaveDirtyPackages(bPromptUserToSave, bSaveMapPackages, bSaveContentPackages, bFastSave, bNotifyNoPackagesSaved, bCanBeDeclined, &bHadPackagesToSave);
103 |
104 | // bSaved can be true if the user selects to not save an asset by unchecking it and clicking "save"
105 | if (bSaved)
106 | {
107 | TArray DirtyPackages;
108 | FEditorFileUtils::GetDirtyWorldPackages(DirtyPackages);
109 | FEditorFileUtils::GetDirtyContentPackages(DirtyPackages);
110 | bSaved = DirtyPackages.Num() == 0;
111 | }
112 |
113 | return bSaved;
114 | }
115 |
116 | /// Find all packages in Content directory
117 | TArray FGitSourceControlMenu::ListAllPackages()
118 | {
119 | TArray PackageRelativePaths;
120 | FPackageName::FindPackagesInDirectory(PackageRelativePaths, *FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()));
121 |
122 | TArray PackageNames;
123 | PackageNames.Reserve(PackageRelativePaths.Num());
124 | for (const FString& Path : PackageRelativePaths)
125 | {
126 | FString PackageName;
127 | FString FailureReason;
128 | if (FPackageName::TryConvertFilenameToLongPackageName(Path, PackageName, &FailureReason))
129 | {
130 | PackageNames.Add(PackageName);
131 | }
132 | else
133 | {
134 | FMessageLog("SourceControl").Error(FText::FromString(FailureReason));
135 | }
136 | }
137 |
138 | return PackageNames;
139 | }
140 |
141 | /// Unkink all loaded packages to allow to update them
142 | TArray FGitSourceControlMenu::UnlinkPackages(const TArray& InPackageNames)
143 | {
144 | TArray LoadedPackages;
145 |
146 | // Inspired from ContentBrowserUtils::SyncPathsFromSourceControl()
147 | if (InPackageNames.Num() > 0)
148 | {
149 | // Form a list of loaded packages to reload...
150 | LoadedPackages.Reserve(InPackageNames.Num());
151 | for (const FString& PackageName : InPackageNames)
152 | {
153 | UPackage* Package = FindPackage(nullptr, *PackageName);
154 | if (Package)
155 | {
156 | LoadedPackages.Emplace(Package);
157 |
158 | // Detach the linkers of any loaded packages so that SCC can overwrite the files...
159 | if (!Package->IsFullyLoaded())
160 | {
161 | FlushAsyncLoading();
162 | Package->FullyLoad();
163 | }
164 | ResetLoaders(Package);
165 | }
166 | }
167 | UE_LOG(LogSourceControl, Log, TEXT("Reseted Loader for %d Packages"), LoadedPackages.Num());
168 | }
169 |
170 | return LoadedPackages;
171 | }
172 |
173 | void FGitSourceControlMenu::ReloadPackages(TArray& InPackagesToReload)
174 | {
175 | UE_LOG(LogSourceControl, Log, TEXT("Reloading %d Packages..."), InPackagesToReload.Num());
176 |
177 | // Syncing may have deleted some packages, so we need to unload those rather than re-load them...
178 | TArray PackagesToUnload;
179 | InPackagesToReload.RemoveAll([&](UPackage* InPackage) -> bool
180 | {
181 | const FString PackageExtension = InPackage->ContainsMap() ? FPackageName::GetMapPackageExtension() : FPackageName::GetAssetPackageExtension();
182 | const FString PackageFilename = FPackageName::LongPackageNameToFilename(InPackage->GetName(), PackageExtension);
183 | if (!FPaths::FileExists(PackageFilename))
184 | {
185 | PackagesToUnload.Emplace(InPackage);
186 | return true; // remove package
187 | }
188 | return false; // keep package
189 | });
190 |
191 | // Hot-reload the new packages...
192 | UPackageTools::ReloadPackages(InPackagesToReload);
193 |
194 | // Unload any deleted packages...
195 | UPackageTools::UnloadPackages(PackagesToUnload);
196 | }
197 |
198 | // Ask the user if he wants to stash any modification and try to unstash them afterward, which could lead to conflicts
199 | bool FGitSourceControlMenu::StashAwayAnyModifications()
200 | {
201 | bool bStashOk = true;
202 |
203 | FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl");
204 | FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
205 | const FString& PathToRespositoryRoot = Provider.GetPathToRepositoryRoot();
206 | const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
207 | const TArray ParametersStatus{"--porcelain --untracked-files=no"};
208 | TArray InfoMessages;
209 | TArray ErrorMessages;
210 | // Check if there is any modification to the working tree
211 | const bool bStatusOk = GitSourceControlUtils::RunCommand(TEXT("status"), PathToGitBinary, PathToRespositoryRoot, ParametersStatus, TArray(), InfoMessages, ErrorMessages);
212 | if ((bStatusOk) && (InfoMessages.Num() > 0))
213 | {
214 | // Ask the user before stashing
215 | const FText DialogText(LOCTEXT("SourceControlMenu_Stash_Ask", "Stash (save) all modifications of the working tree? Required to Sync/Pull!"));
216 | const EAppReturnType::Type Choice = FMessageDialog::Open(EAppMsgType::OkCancel, DialogText);
217 | if (Choice == EAppReturnType::Ok)
218 | {
219 | const TArray ParametersStash{ "save \"Stashed by Unreal Engine Git Plugin\"" };
220 | bStashMadeBeforeSync = GitSourceControlUtils::RunCommand(TEXT("stash"), PathToGitBinary, PathToRespositoryRoot, ParametersStash, TArray(), InfoMessages, ErrorMessages);
221 | if (!bStashMadeBeforeSync)
222 | {
223 | FMessageLog SourceControlLog("SourceControl");
224 | SourceControlLog.Warning(LOCTEXT("SourceControlMenu_StashFailed", "Stashing away modifications failed!"));
225 | SourceControlLog.Notify();
226 | }
227 | }
228 | else
229 | {
230 | bStashOk = false;
231 | }
232 | }
233 |
234 | return bStashOk;
235 | }
236 |
237 | // Unstash any modifications if a stash was made at the beginning of the Sync operation
238 | void FGitSourceControlMenu::ReApplyStashedModifications()
239 | {
240 | if (bStashMadeBeforeSync)
241 | {
242 | FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl");
243 | FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
244 | const FString& PathToRespositoryRoot = Provider.GetPathToRepositoryRoot();
245 | const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
246 | const TArray ParametersStash{ "pop" };
247 | TArray InfoMessages;
248 | TArray ErrorMessages;
249 | const bool bUnstashOk = GitSourceControlUtils::RunCommand(TEXT("stash"), PathToGitBinary, PathToRespositoryRoot, ParametersStash, TArray(), InfoMessages, ErrorMessages);
250 | if (!bUnstashOk)
251 | {
252 | FMessageLog SourceControlLog("SourceControl");
253 | SourceControlLog.Warning(LOCTEXT("SourceControlMenu_UnstashFailed", "Unstashing previously saved modifications failed!"));
254 | SourceControlLog.Notify();
255 | }
256 | }
257 | }
258 |
259 | void FGitSourceControlMenu::SyncClicked()
260 | {
261 | if (!OperationInProgressNotification.IsValid())
262 | {
263 | // Ask the user to save any dirty assets opened in Editor
264 | const bool bSaved = SaveDirtyPackages();
265 | if (bSaved)
266 | {
267 | // Better way to do a pull/sync
268 | // First fetch and check what files have really changed
269 | // Then unload only the necessary packages instead of everything
270 | // Then reload the changed package.
271 | FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl");
272 | FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
273 | const FString& PathToRepositoryRoot = Provider.GetPathToRepositoryRoot();
274 | const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
275 |
276 | FString BranchName;
277 | GitSourceControlUtils::GetBranchName(PathToGitBinary, PathToRepositoryRoot, BranchName);
278 |
279 | FString DiffOrigin("..origin/");
280 | DiffOrigin.Append(BranchName);
281 |
282 | TArray ChangedFiles;
283 | {
284 | TArray ErrorMessages;
285 | TArray Parameters;
286 | Parameters.Add(TEXT("--stat"));
287 | Parameters.Add(TEXT("--name-only"));
288 | Parameters.Add(DiffOrigin);
289 |
290 | // Get changed files remote and on local commits (we need to determine which ones are local commits)
291 | GitSourceControlUtils::RunCommand(TEXT("diff"), PathToGitBinary, PathToRepositoryRoot, Parameters, TArray(), ChangedFiles, ErrorMessages);
292 |
293 | // Handle uncommitted changes
294 | TArray LocalChangedFiles;
295 | const TArray ParametersStatus{"--porcelain --untracked-files=no"};
296 | GitSourceControlUtils::RunCommand(TEXT("status"), PathToGitBinary, PathToRepositoryRoot, ParametersStatus, TArray(), LocalChangedFiles, ErrorMessages);
297 |
298 | for(FString& Filename: LocalChangedFiles)
299 | {
300 | if(Filename.StartsWith(" M") || Filename.StartsWith(" A"))
301 | {
302 | ChangedFiles.Add(Filename.RightChop(3));
303 | }
304 | }
305 | }
306 |
307 | // TODO: Handle local commits
308 | TArray PackagesToUnlink;
309 | for(FString& Filename: ChangedFiles)
310 | {
311 | FString AbsolutePath = FPaths::ConvertRelativePathToFull(PathToRepositoryRoot, Filename);
312 | FString PackageName;
313 | if(FPackageName::TryConvertFilenameToLongPackageName(AbsolutePath, PackageName))
314 | {
315 | UE_LOG(LogTemp, Warning, TEXT("%s - %s"), *AbsolutePath, *PackageName);
316 | PackagesToUnlink.Add(PackageName);
317 | }
318 | }
319 |
320 | PackagesToReload = UnlinkPackages(PackagesToUnlink);
321 |
322 | // // Ask the user if he wants to stash any modification and try to unstash them afterward, which could lead to conflicts
323 | const bool bStashed = StashAwayAnyModifications();
324 | if (bStashed)
325 | {
326 | TSharedRef SyncOperation = ISourceControlOperation::Create();
327 | #if ENGINE_MAJOR_VERSION == 5
328 | const ECommandResult::Type Result = Provider.Execute(SyncOperation, FSourceControlChangelistPtr(), TArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete));
329 | #else
330 | const ECommandResult::Type Result = Provider.Execute(SyncOperation, TArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete));
331 | #endif
332 | if (Result == ECommandResult::Succeeded)
333 | {
334 | // Display an ongoing notification during the whole operation (packages will be reloaded at the completion of the operation)
335 | DisplayInProgressNotification(SyncOperation->GetInProgressString());
336 | }
337 | else
338 | {
339 | // Report failure with a notification and Reload all packages
340 | DisplayFailureNotification(SyncOperation->GetName());
341 | ReloadPackages(PackagesToReload);
342 | }
343 | }
344 | else
345 | {
346 | FMessageLog SourceControlLog("SourceControl");
347 | SourceControlLog.Warning(LOCTEXT("SourceControlMenu_Sync_Unsaved", "Stash away all modifications before attempting to Sync!"));
348 | SourceControlLog.Notify();
349 | }
350 | }
351 | else
352 | {
353 | FMessageLog SourceControlLog("SourceControl");
354 | SourceControlLog.Warning(LOCTEXT("SourceControlMenu_Sync_Unsaved", "Save All Assets before attempting to Sync!"));
355 | SourceControlLog.Notify();
356 | }
357 | }
358 | else
359 | {
360 | FMessageLog SourceControlLog("SourceControl");
361 | SourceControlLog.Warning(LOCTEXT("SourceControlMenu_InProgress", "Source control operation already in progress"));
362 | SourceControlLog.Notify();
363 | }
364 | }
365 |
366 | void FGitSourceControlMenu::PushClicked()
367 | {
368 | if (!OperationInProgressNotification.IsValid())
369 | {
370 | // Launch a "Push" Operation
371 | FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl");
372 | FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
373 | TSharedRef PushOperation = ISourceControlOperation::Create();
374 | #if ENGINE_MAJOR_VERSION == 5
375 | const ECommandResult::Type Result = Provider.Execute(PushOperation, FSourceControlChangelistPtr(), TArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete));
376 | #else
377 | const ECommandResult::Type Result = Provider.Execute(PushOperation, TArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete));
378 | #endif
379 | if (Result == ECommandResult::Succeeded)
380 | {
381 | // Display an ongoing notification during the whole operation
382 | DisplayInProgressNotification(PushOperation->GetInProgressString());
383 | }
384 | else
385 | {
386 | // Report failure with a notification
387 | DisplayFailureNotification(PushOperation->GetName());
388 | }
389 | }
390 | else
391 | {
392 | FMessageLog SourceControlLog("SourceControl");
393 | SourceControlLog.Warning(LOCTEXT("SourceControlMenu_InProgress", "Source control operation already in progress"));
394 | SourceControlLog.Notify();
395 | }
396 | }
397 |
398 | void FGitSourceControlMenu::RevertClicked()
399 | {
400 | if (!OperationInProgressNotification.IsValid())
401 | {
402 | // Ask the user before reverting all!
403 | const FText DialogText(LOCTEXT("SourceControlMenu_Revert_Ask", "Revert all modifications of the working tree?"));
404 | const EAppReturnType::Type Choice = FMessageDialog::Open(EAppMsgType::OkCancel, DialogText);
405 | if (Choice == EAppReturnType::Ok)
406 | {
407 | // NOTE No need to force the user to SaveDirtyPackages(); since he will be presented with a choice by the Editor
408 |
409 | // Find and Unlink all packages in Content directory to allow to update them
410 | PackagesToReload = UnlinkPackages(ListAllPackages());
411 |
412 | // Launch a "Revert" Operation
413 | FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl");
414 | FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
415 | TSharedRef RevertOperation = ISourceControlOperation::Create();
416 | #if ENGINE_MAJOR_VERSION == 5
417 | const ECommandResult::Type Result = Provider.Execute(RevertOperation, FSourceControlChangelistPtr(), TArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete));
418 | #else
419 | const ECommandResult::Type Result = Provider.Execute(RevertOperation, TArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete));
420 | #endif
421 | if (Result == ECommandResult::Succeeded)
422 | {
423 | // Display an ongoing notification during the whole operation
424 | DisplayInProgressNotification(RevertOperation->GetInProgressString());
425 | }
426 | else
427 | {
428 | // Report failure with a notification and Reload all packages
429 | DisplayFailureNotification(RevertOperation->GetName());
430 | ReloadPackages(PackagesToReload);
431 | }
432 | }
433 | }
434 | else
435 | {
436 | FMessageLog SourceControlLog("SourceControl");
437 | SourceControlLog.Warning(LOCTEXT("SourceControlMenu_InProgress", "Source control operation already in progress"));
438 | SourceControlLog.Notify();
439 | }
440 | }
441 |
442 | void FGitSourceControlMenu::RefreshClicked()
443 | {
444 | if (!OperationInProgressNotification.IsValid())
445 | {
446 | // Launch an "UpdateStatus" Operation
447 | FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl");
448 | FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
449 | TSharedRef RefreshOperation = ISourceControlOperation::Create();
450 | RefreshOperation->SetCheckingAllFiles(true);
451 | #if ENGINE_MAJOR_VERSION == 5
452 | const ECommandResult::Type Result = Provider.Execute(RefreshOperation, FSourceControlChangelistPtr(), TArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete));
453 | #else
454 | const ECommandResult::Type Result = Provider.Execute(RefreshOperation, TArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete));
455 | #endif
456 | if (Result == ECommandResult::Succeeded)
457 | {
458 | // Display an ongoing notification during the whole operation
459 | DisplayInProgressNotification(RefreshOperation->GetInProgressString());
460 | }
461 | else
462 | {
463 | // Report failure with a notification
464 | DisplayFailureNotification(RefreshOperation->GetName());
465 | }
466 | }
467 | else
468 | {
469 | FMessageLog SourceControlLog("SourceControl");
470 | SourceControlLog.Warning(LOCTEXT("SourceControlMenu_InProgress", "Source control operation already in progress"));
471 | SourceControlLog.Notify();
472 | }
473 | }
474 |
475 | // Display an ongoing notification during the whole operation
476 | void FGitSourceControlMenu::DisplayInProgressNotification(const FText& InOperationInProgressString)
477 | {
478 | if (!OperationInProgressNotification.IsValid())
479 | {
480 | FNotificationInfo Info(InOperationInProgressString);
481 | Info.bFireAndForget = false;
482 | Info.ExpireDuration = 0.0f;
483 | Info.FadeOutDuration = 1.0f;
484 | OperationInProgressNotification = FSlateNotificationManager::Get().AddNotification(Info);
485 | if (OperationInProgressNotification.IsValid())
486 | {
487 | OperationInProgressNotification.Pin()->SetCompletionState(SNotificationItem::CS_Pending);
488 | }
489 | }
490 | }
491 |
492 | // Remove the ongoing notification at the end of the operation
493 | void FGitSourceControlMenu::RemoveInProgressNotification()
494 | {
495 | if (OperationInProgressNotification.IsValid())
496 | {
497 | OperationInProgressNotification.Pin()->ExpireAndFadeout();
498 | OperationInProgressNotification.Reset();
499 | }
500 | }
501 |
502 | // Display a temporary success notification at the end of the operation
503 | void FGitSourceControlMenu::DisplaySucessNotification(const FName& InOperationName)
504 | {
505 | const FText NotificationText = FText::Format(
506 | LOCTEXT("SourceControlMenu_Success", "{0} operation was successful!"),
507 | FText::FromName(InOperationName)
508 | );
509 | FNotificationInfo Info(NotificationText);
510 | Info.bUseSuccessFailIcons = true;
511 | #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
512 | Info.Image = FAppStyle::GetBrush(TEXT("NotificationList.SuccessImage"));
513 | #else
514 | Info.Image = FEditorStyle::GetBrush(TEXT("NotificationList.SuccessImage"));
515 | #endif
516 | FSlateNotificationManager::Get().AddNotification(Info);
517 | UE_LOG(LogSourceControl, Log, TEXT("%s"), *NotificationText.ToString());
518 | }
519 |
520 | // Display a temporary failure notification at the end of the operation
521 | void FGitSourceControlMenu::DisplayFailureNotification(const FName& InOperationName)
522 | {
523 | const FText NotificationText = FText::Format(
524 | LOCTEXT("SourceControlMenu_Failure", "Error: {0} operation failed!"),
525 | FText::FromName(InOperationName)
526 | );
527 | FNotificationInfo Info(NotificationText);
528 | Info.ExpireDuration = 8.0f;
529 | FSlateNotificationManager::Get().AddNotification(Info);
530 | UE_LOG(LogSourceControl, Error, TEXT("%s"), *NotificationText.ToString());
531 | }
532 |
533 | void FGitSourceControlMenu::OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult)
534 | {
535 | RemoveInProgressNotification();
536 |
537 | if ((InOperation->GetName() == "Sync") || (InOperation->GetName() == "Revert"))
538 | {
539 | // Unstash any modifications if a stash was made at the beginning of the Sync operation
540 | ReApplyStashedModifications();
541 | // Reload packages that where unlinked at the beginning of the Sync/Revert operation
542 | ReloadPackages(PackagesToReload);
543 | }
544 |
545 | // Report result with a notification
546 | if (InResult == ECommandResult::Succeeded)
547 | {
548 | DisplaySucessNotification(InOperation->GetName());
549 | }
550 | else
551 | {
552 | DisplayFailureNotification(InOperation->GetName());
553 | }
554 | }
555 |
556 | #if ENGINE_MAJOR_VERSION == 5
557 | void FGitSourceControlMenu::AddMenuExtension(FToolMenuSection& Builder)
558 | #else
559 | void FGitSourceControlMenu::AddMenuExtension(FMenuBuilder& Builder)
560 | #endif
561 | {
562 | Builder.AddMenuEntry(
563 | #if ENGINE_MAJOR_VERSION == 5
564 | "GitPush",
565 | #endif
566 |
567 | LOCTEXT("GitPush", "Push"),
568 | LOCTEXT("GitPushTooltip", "Push all local commits to the remote server."),
569 | #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
570 | FSlateIcon(FAppStyle::GetAppStyleSetName(), "SourceControl.Actions.Submit"),
571 | #else
572 | FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Submit"),
573 | #endif
574 | FUIAction(
575 | FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::PushClicked),
576 | FCanExecuteAction::CreateRaw(this, &FGitSourceControlMenu::HaveRemoteUrl)
577 | )
578 | );
579 |
580 | Builder.AddMenuEntry(
581 | #if ENGINE_MAJOR_VERSION == 5
582 | "GitSync",
583 | #endif
584 | LOCTEXT("GitSync", "Sync/Pull"),
585 | LOCTEXT("GitSyncTooltip", "Update all files in the local repository to the latest version of the remote server."),
586 | #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
587 | FSlateIcon(FAppStyle::GetAppStyleSetName(), "SourceControl.Actions.Sync"),
588 | #else
589 | FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Sync"),
590 | #endif
591 | FUIAction(
592 | FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::SyncClicked),
593 | FCanExecuteAction::CreateRaw(this, &FGitSourceControlMenu::HaveRemoteUrl)
594 | )
595 | );
596 |
597 | Builder.AddMenuEntry(
598 | #if ENGINE_MAJOR_VERSION == 5
599 | "GitRevert",
600 | #endif
601 | LOCTEXT("GitRevert", "Revert"),
602 | LOCTEXT("GitRevertTooltip", "Revert all files in the repository to their unchanged state."),
603 | #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
604 | FSlateIcon(FAppStyle::GetAppStyleSetName(), "SourceControl.Actions.Revert"),
605 | #else
606 | FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Revert"),
607 | #endif
608 | FUIAction(
609 | FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::RevertClicked),
610 | FCanExecuteAction()
611 | )
612 | );
613 |
614 | Builder.AddMenuEntry(
615 | #if ENGINE_MAJOR_VERSION == 5
616 | "GitRefresh",
617 | #endif
618 | LOCTEXT("GitRefresh", "Refresh"),
619 | LOCTEXT("GitRefreshTooltip", "Update the source control status of all files in the local repository."),
620 | #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
621 | FSlateIcon(FAppStyle::GetAppStyleSetName(), "SourceControl.Actions.Refresh"),
622 | #else
623 | FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Refresh"),
624 | #endif
625 | FUIAction(
626 | FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::RefreshClicked),
627 | FCanExecuteAction()
628 | )
629 | );
630 | }
631 |
632 | #if ENGINE_MAJOR_VERSION == 4
633 | TSharedRef FGitSourceControlMenu::OnExtendLevelEditorViewMenu(const TSharedRef CommandList)
634 | {
635 | TSharedRef Extender(new FExtender());
636 |
637 | Extender->AddMenuExtension(
638 | "SourceControlActions",
639 | EExtensionHook::After,
640 | nullptr,
641 | FMenuExtensionDelegate::CreateRaw(this, &FGitSourceControlMenu::AddMenuExtension));
642 |
643 | return Extender;
644 | }
645 | #endif
646 |
647 | #undef LOCTEXT_NAMESPACE
648 |
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlMenu.h:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 | //
3 | // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
4 | // or copy at http://opensource.org/licenses/MIT)
5 |
6 | #pragma once
7 |
8 | #include "CoreMinimal.h"
9 | #include "ISourceControlProvider.h"
10 |
11 | #include "Runtime/Launch/Resources/Version.h"
12 |
13 | /** Git extension of the Source Control toolbar menu */
14 | class FGitSourceControlMenu
15 | {
16 | public:
17 | void Register();
18 | void Unregister();
19 |
20 | /** This functions will be bound to appropriate Command. */
21 | void PushClicked();
22 | void SyncClicked();
23 | void RevertClicked();
24 | void RefreshClicked();
25 |
26 | private:
27 | bool HaveRemoteUrl() const;
28 |
29 | bool SaveDirtyPackages();
30 | TArray ListAllPackages();
31 | TArray UnlinkPackages(const TArray& InPackageNames);
32 | void ReloadPackages(TArray& InPackagesToReload);
33 |
34 | bool StashAwayAnyModifications();
35 | void ReApplyStashedModifications();
36 |
37 | #if ENGINE_MAJOR_VERSION == 5
38 | void AddMenuExtension(struct FToolMenuSection& Builder);
39 | #else
40 | void AddMenuExtension(class FMenuBuilder& Builder);
41 | TSharedRef OnExtendLevelEditorViewMenu(const TSharedRef CommandList);
42 | #endif
43 |
44 | void DisplayInProgressNotification(const FText& InOperationInProgressString);
45 | void RemoveInProgressNotification();
46 | void DisplaySucessNotification(const FName& InOperationName);
47 | void DisplayFailureNotification(const FName& InOperationName);
48 |
49 | private:
50 | #if ENGINE_MAJOR_VERSION == 4
51 | FDelegateHandle ViewMenuExtenderHandle;
52 | #endif
53 |
54 | /** Was there a need to stash away modifications before Sync? */
55 | bool bStashMadeBeforeSync;
56 |
57 | /** Loaded packages to reload after a Sync or Revert operation */
58 | TArray PackagesToReload;
59 |
60 | /** Current source control operation from extended menu if any */
61 | TWeakPtr OperationInProgressNotification;
62 |
63 | /** Delegate called when a source control operation has completed */
64 | void OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult);
65 | };
66 |
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlModule.cpp:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 | //
3 | // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
4 | // or copy at http://opensource.org/licenses/MIT)
5 |
6 | #include "GitSourceControlModule.h"
7 |
8 | #include "Misc/App.h"
9 | #include "Modules/ModuleManager.h"
10 | #include "GitSourceControlOperations.h"
11 | #include "Features/IModularFeatures.h"
12 |
13 | #define LOCTEXT_NAMESPACE "GitSourceControl"
14 |
15 | template
16 | static TSharedRef CreateWorker()
17 | {
18 | return MakeShareable( new Type() );
19 | }
20 |
21 | void FGitSourceControlModule::StartupModule()
22 | {
23 | // Register our operations (implemented in GitSourceControlOperations.cpp by subclassing from Engine\Source\Developer\SourceControl\Public\SourceControlOperations.h)
24 | GitSourceControlProvider.RegisterWorker( "Connect", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) );
25 | // 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).
26 | GitSourceControlProvider.RegisterWorker( "CheckOut", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) );
27 | GitSourceControlProvider.RegisterWorker( "UpdateStatus", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) );
28 | GitSourceControlProvider.RegisterWorker( "MarkForAdd", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) );
29 | GitSourceControlProvider.RegisterWorker( "Delete", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) );
30 | GitSourceControlProvider.RegisterWorker( "Revert", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) );
31 | GitSourceControlProvider.RegisterWorker( "Sync", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) );
32 | GitSourceControlProvider.RegisterWorker( "Push", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) );
33 | GitSourceControlProvider.RegisterWorker( "CheckIn", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) );
34 | GitSourceControlProvider.RegisterWorker( "Copy", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) );
35 | GitSourceControlProvider.RegisterWorker( "Resolve", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) );
36 |
37 | // load our settings
38 | GitSourceControlSettings.LoadSettings();
39 |
40 | // Bind our source control provider to the editor
41 | IModularFeatures::Get().RegisterModularFeature( "SourceControl", &GitSourceControlProvider );
42 | }
43 |
44 | void FGitSourceControlModule::ShutdownModule()
45 | {
46 | // shut down the provider, as this module is going away
47 | GitSourceControlProvider.Close();
48 |
49 | // unbind provider from editor
50 | IModularFeatures::Get().UnregisterModularFeature("SourceControl", &GitSourceControlProvider);
51 | }
52 |
53 | void FGitSourceControlModule::SaveSettings()
54 | {
55 | if (FApp::IsUnattended() || IsRunningCommandlet())
56 | {
57 | return;
58 | }
59 |
60 | GitSourceControlSettings.SaveSettings();
61 | }
62 |
63 | IMPLEMENT_MODULE(FGitSourceControlModule, GitSourceControl);
64 |
65 | #undef LOCTEXT_NAMESPACE
66 |
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlModule.h:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 | //
3 | // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
4 | // or copy at http://opensource.org/licenses/MIT)
5 |
6 | #pragma once
7 |
8 | #include "CoreMinimal.h"
9 | #include "Modules/ModuleInterface.h"
10 | #include "GitSourceControlSettings.h"
11 | #include "GitSourceControlProvider.h"
12 |
13 | /**
14 |
15 | UE4GitPlugin is a simple Git Source Control Plugin for Unreal Engine
16 |
17 | Written and contributed by Sebastien Rombauts (sebastien.rombauts@gmail.com)
18 |
19 | ### Supported features
20 | - initialize a new Git local repository ('git init') to manage your Unreal Engine 4 and 5 Game Project
21 | - can also create an appropriate .gitignore file as part of initialization
22 | - can also create a .gitattributes file to enable Git LFS (Large File System) as part of initialization
23 | - can also make the initial commit, with custom multi-line message
24 | - can also configure the default remote origin URL
25 | - display status icons to show modified/added/deleted/untracked files
26 | - show history of a file
27 | - visual diff of a blueprint against depot or between previous versions of a file
28 | - revert modifications of a file
29 | - add, delete, rename a file
30 | - checkin/commit a file (cannot handle atomically more than 50 files)
31 | - migrate an asset between two projects if both are using Git
32 | - solve a merge conflict on a blueprint
33 | - show current branch name in status text
34 | - Sync to Pull (rebase) the current branch
35 | - Git LFS (Github, Gitlab, Bitbucket) is working with Git 2.10+ under Windows
36 | - Git LFS 2 File Locking is working with Git 2.10+ and Git LFS 2.0.0
37 | - Windows, Mac and Linux
38 |
39 | ### TODO
40 | 1. configure the name of the remote instead of default "origin"
41 |
42 | ### TODO LFS 2.x File Locking
43 |
44 | Known issues:
45 | 0. False error logs after a successful push:
46 | To https://github.com/SRombauts/UE4GitLfs2FileLocks.git
47 | ee44ff5..59da15e HEAD -> master
48 |
49 | Use "TODO LFS" in the code to track things left to do/improve/refactor:
50 | 1. IsUsingGitLfsLocking() should be cached in the Provider to avoid calling AccessSettings() too frequently
51 | it can not change without re-initializing (at least re-connect) the Provider!
52 | 2. Implement FGitSourceControlProvider::bWorkingOffline like the SubversionSourceControl plugin
53 | 3. Trying to deactivate Git LFS 2 file locking afterward on the "Login to Source Control" (Connect/Configure) screen
54 | is not working after Git LFS 2 has switched "read-only" flag on files (which needs the Checkout operation to be editable)!
55 | - temporarily deactivating locks may be required if we want to be able to work while not connected (do we really need this ???)
56 | - does Git LFS have a command to do this deactivation ?
57 | - perhaps should we rely on detection of such flags to detect LFS 2 usage (ie. the need to do a checkout)
58 | - see SubversionSourceControl plugin that deals with such flags
59 | - 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
60 | - else we should at least revert those read-only flags when going out of "Lock mode"
61 | 4. Optimize usage of "git lfs locks", ie reduce the use of UdpateStatus() in Operations
62 |
63 | ### What *cannot* be done presently
64 | - Branch/Merge are not in the current Editor workflow
65 | - Fetch is not in the current Editor workflow
66 | - Amend a commit is not in the current Editor workflow
67 | - Configure user name & email ('git config user.name' & git config user.email')
68 |
69 | ### Known issues
70 | - the Editor does not show deleted files (only when deleted externally?)
71 | - the Editor does not show missing files
72 | - missing localization for git specific messages
73 | - displaying states of 'Engine' assets (also needs management of 'out of tree' files)
74 | - renaming a Blueprint in Editor leaves a redirector file, AND modify too much the asset to enable git to track its history through renaming
75 | - standard Editor commit dialog asks if user wants to "Keep Files Checked Out" => no use for Git or Mercurial CanCheckOut()==false
76 |
77 | */
78 | class FGitSourceControlModule : public IModuleInterface
79 | {
80 | public:
81 | /** IModuleInterface implementation */
82 | virtual void StartupModule() override;
83 | virtual void ShutdownModule() override;
84 |
85 | /** Access the Git source control settings */
86 | FGitSourceControlSettings& AccessSettings()
87 | {
88 | return GitSourceControlSettings;
89 | }
90 |
91 | const FGitSourceControlSettings& AccessSettings() const
92 | {
93 | return GitSourceControlSettings;
94 | }
95 |
96 | /** Save the Git source control settings */
97 | void SaveSettings();
98 |
99 | /** Access the Git source control provider */
100 | FGitSourceControlProvider& GetProvider()
101 | {
102 | return GitSourceControlProvider;
103 | }
104 |
105 | const FGitSourceControlProvider& GetProvider() const
106 | {
107 | return GitSourceControlProvider;
108 | }
109 |
110 | private:
111 | /** The Git source control provider */
112 | FGitSourceControlProvider GitSourceControlProvider;
113 |
114 | /** The settings for Git source control */
115 | FGitSourceControlSettings GitSourceControlSettings;
116 | };
117 |
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlOperations.cpp:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 | //
3 | // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
4 | // or copy at http://opensource.org/licenses/MIT)
5 |
6 | #include "GitSourceControlOperations.h"
7 |
8 | #include "Misc/Paths.h"
9 | #include "Modules/ModuleManager.h"
10 | #include "SourceControlOperations.h"
11 | #include "ISourceControlModule.h"
12 | #include "GitSourceControlModule.h"
13 | #include "GitSourceControlCommand.h"
14 | #include "GitSourceControlUtils.h"
15 | #include "Logging/MessageLog.h"
16 | #include "Misc/MessageDialog.h"
17 |
18 | #define LOCTEXT_NAMESPACE "GitSourceControl"
19 |
20 | FName FGitPush::GetName() const
21 | {
22 | return "Push";
23 | }
24 |
25 | FText FGitPush::GetInProgressString() const
26 | {
27 | // TODO Configure origin
28 | return LOCTEXT("SourceControl_Push", "Pushing local commits to remote origin...");
29 | }
30 |
31 |
32 | FName FGitConnectWorker::GetName() const
33 | {
34 | return "Connect";
35 | }
36 |
37 | bool FGitConnectWorker::Execute(FGitSourceControlCommand& InCommand)
38 | {
39 | check(InCommand.Operation->GetName() == GetName());
40 | TSharedRef Operation = StaticCastSharedRef(InCommand.Operation);
41 |
42 | // Check Git Availability
43 | if((InCommand.PathToGitBinary.Len() > 0) && GitSourceControlUtils::CheckGitAvailability(InCommand.PathToGitBinary))
44 | {
45 | // Now update the status of assets in Content/ directory and also Config files
46 | TArray ProjectDirs;
47 | ProjectDirs.Add(FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()));
48 | ProjectDirs.Add(FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir()));
49 | InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, ProjectDirs, InCommand.ErrorMessages, States);
50 | if(!InCommand.bCommandSuccessful || InCommand.ErrorMessages.Num() > 0)
51 | {
52 | Operation->SetErrorText(LOCTEXT("NotAGitRepository", "Failed to enable Git source control. You need to initialize the project as a Git repository first."));
53 | InCommand.bCommandSuccessful = false;
54 | }
55 | else
56 | {
57 | GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary);
58 |
59 | if(InCommand.bUsingGitLfsLocking)
60 | {
61 | // Check server connection by checking lock status (when using Git LFS file Locking worflow)
62 | InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("lfs locks"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), TArray(), InCommand.InfoMessages, InCommand.ErrorMessages);
63 | }
64 | }
65 | }
66 | else
67 | {
68 | Operation->SetErrorText(LOCTEXT("GitNotFound", "Failed to enable Git source control. You need to install Git and specify a valid path to git executable."));
69 | InCommand.bCommandSuccessful = false;
70 | }
71 |
72 | return InCommand.bCommandSuccessful;
73 | }
74 |
75 | bool FGitConnectWorker::UpdateStates() const
76 | {
77 | return GitSourceControlUtils::UpdateCachedStates(States);
78 | }
79 |
80 | FName FGitCheckOutWorker::GetName() const
81 | {
82 | return "CheckOut";
83 | }
84 |
85 | bool FGitCheckOutWorker::Execute(FGitSourceControlCommand& InCommand)
86 | {
87 | check(InCommand.Operation->GetName() == GetName());
88 |
89 | if(InCommand.bUsingGitLfsLocking)
90 | {
91 | // lock files: execute the LFS command on relative filenames
92 | InCommand.bCommandSuccessful = true;
93 | const TArray RelativeFiles = GitSourceControlUtils::RelativeFilenames(InCommand.Files, InCommand.PathToRepositoryRoot);
94 | for(const auto& RelativeFile : RelativeFiles)
95 | {
96 | TArray OneFile;
97 | OneFile.Add(RelativeFile);
98 | InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("lfs lock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), OneFile, InCommand.InfoMessages, InCommand.ErrorMessages);
99 | }
100 |
101 | // now update the status of our files
102 | GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States);
103 | }
104 | else
105 | {
106 | InCommand.bCommandSuccessful = false;
107 | }
108 |
109 | return InCommand.bCommandSuccessful;
110 | }
111 |
112 | bool FGitCheckOutWorker::UpdateStates() const
113 | {
114 | return GitSourceControlUtils::UpdateCachedStates(States);
115 | }
116 |
117 | static FText ParseCommitResults(const TArray& InResults)
118 | {
119 | if(InResults.Num() >= 1)
120 | {
121 | const FString& FirstLine = InResults[0];
122 | return FText::Format(LOCTEXT("CommitMessage", "Commited {0}."), FText::FromString(FirstLine));
123 | }
124 | return LOCTEXT("CommitMessageUnknown", "Submitted revision.");
125 | }
126 |
127 | // Get Locked Files (that is, CheckedOut files, not Added ones)
128 | const TArray GetLockedFiles(const TArray& InFiles)
129 | {
130 | TArray LockedFiles;
131 |
132 | FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl");
133 | FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
134 |
135 | TArray> LocalStates;
136 | Provider.GetState(InFiles, LocalStates, EStateCacheUsage::Use);
137 | for(const auto& State : LocalStates)
138 | {
139 | if(State->IsCheckedOut())
140 | {
141 | LockedFiles.Add(State->GetFilename());
142 | }
143 | }
144 |
145 | return LockedFiles;
146 | }
147 |
148 | FName FGitCheckInWorker::GetName() const
149 | {
150 | return "CheckIn";
151 | }
152 |
153 | bool FGitCheckInWorker::Execute(FGitSourceControlCommand& InCommand)
154 | {
155 | check(InCommand.Operation->GetName() == GetName());
156 |
157 | TSharedRef Operation = StaticCastSharedRef(InCommand.Operation);
158 |
159 | // make a temp file to place our commit message in
160 | FGitScopedTempFile CommitMsgFile(Operation->GetDescription());
161 | if(CommitMsgFile.GetFilename().Len() > 0)
162 | {
163 | TArray Parameters;
164 | FString ParamCommitMsgFilename = TEXT("--file=\"");
165 | ParamCommitMsgFilename += FPaths::ConvertRelativePathToFull(CommitMsgFile.GetFilename());
166 | ParamCommitMsgFilename += TEXT("\"");
167 | Parameters.Add(ParamCommitMsgFilename);
168 |
169 | InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommit(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters, InCommand.Files, InCommand.InfoMessages, InCommand.ErrorMessages);
170 | if(InCommand.bCommandSuccessful)
171 | {
172 | // Remove any deleted files from status cache
173 | FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl");
174 | FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
175 |
176 | TArray> LocalStates;
177 | Provider.GetState(InCommand.Files, LocalStates, EStateCacheUsage::Use);
178 | for(const auto& State : LocalStates)
179 | {
180 | if(State->IsDeleted())
181 | {
182 | Provider.RemoveFileFromCache(State->GetFilename());
183 | }
184 | }
185 |
186 | Operation->SetSuccessMessage(ParseCommitResults(InCommand.InfoMessages));
187 | const FString Message = (InCommand.InfoMessages.Num() > 0) ? InCommand.InfoMessages[0] : TEXT("");
188 | UE_LOG(LogSourceControl, Log, TEXT("commit successful: %s"), *Message);
189 |
190 | // git-lfs: push and unlock files
191 | if(InCommand.bUsingGitLfsLocking &&
192 | InCommand.bCommandSuccessful &&
193 | GitSourceControl.AccessSettings().IsPushAfterCommitEnabled())
194 | {
195 | TArray Parameters2;
196 | // TODO Configure origin
197 | Parameters2.Add(TEXT("origin"));
198 | Parameters2.Add(TEXT("HEAD"));
199 | InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("push"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters2, TArray(), InCommand.InfoMessages, InCommand.ErrorMessages);
200 | if(!InCommand.bCommandSuccessful)
201 | {
202 | // if out of date, pull first, then try again
203 | bool bWasOutOfDate = false;
204 | for (const auto& PushError : InCommand.ErrorMessages)
205 | {
206 | if (PushError.Contains(TEXT("[rejected]")) && PushError.Contains(TEXT("non-fast-forward")))
207 | {
208 | // Don't do it during iteration, want to append pull results to InCommand.ErrorMessages
209 | bWasOutOfDate = true;
210 | break;
211 | }
212 | }
213 | if (bWasOutOfDate)
214 | {
215 | // Trying to resolve this automatically can cause too many problems because UE being open prevents
216 | // LFS files from being replaced, meaning the rebase fails which is a nasty place to be for
217 | // unfamiliar Git users. Better to ask them to close UE and pull externally to be safe
218 | FText PushFailMessage(LOCTEXT("GitPush_OutOfDate_Msg", "Git Push failed because there are changes you need to pull. \n"
219 | "However, pulling while the Unreal Editor is open can cause conflicts since files cannot always be replaced. We recommend"
220 | "you exit the editor, and run the following command:\n\n"
221 | " git pull --rebase --autostash\n\n"
222 | "Or run the equivalent in a Git GUI client of your choice"));
223 | FText PushFailTitle(LOCTEXT("GitPush_OutOfDate_Title", "Git Pull Required"));
224 | #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
225 | FMessageDialog::Open(EAppMsgType::Ok, PushFailMessage, PushFailTitle);
226 | #else
227 | FMessageDialog::Open(EAppMsgType::Ok, PushFailMessage, &PushFailTitle);
228 | #endif
229 | UE_LOG(LogSourceControl, Log, TEXT("Push failed because we're out of date, prompting user to resolve manually"));
230 | }
231 | }
232 | if(InCommand.bCommandSuccessful)
233 | {
234 | // unlock files: execute the LFS command on relative filenames
235 | // (unlock only locked files, that is, not Added files)
236 | const TArray LockedFiles = GetLockedFiles(InCommand.Files);
237 | if(LockedFiles.Num() > 0)
238 | {
239 | const TArray RelativeFiles = GitSourceControlUtils::RelativeFilenames(LockedFiles, InCommand.PathToRepositoryRoot);
240 | for(const auto& RelativeFile : RelativeFiles)
241 | {
242 | TArray OneFile;
243 | OneFile.Add(RelativeFile);
244 | GitSourceControlUtils::RunCommand(TEXT("lfs unlock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), OneFile, InCommand.InfoMessages, InCommand.ErrorMessages);
245 | }
246 | }
247 | }
248 | }
249 | }
250 | }
251 |
252 | // now update the status of our files
253 | GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States);
254 | GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary);
255 |
256 | return InCommand.bCommandSuccessful;
257 | }
258 |
259 | bool FGitCheckInWorker::UpdateStates() const
260 | {
261 | return GitSourceControlUtils::UpdateCachedStates(States);
262 | }
263 |
264 | FName FGitMarkForAddWorker::GetName() const
265 | {
266 | return "MarkForAdd";
267 | }
268 |
269 | bool FGitMarkForAddWorker::Execute(FGitSourceControlCommand& InCommand)
270 | {
271 | check(InCommand.Operation->GetName() == GetName());
272 |
273 | InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), InCommand.Files, InCommand.InfoMessages, InCommand.ErrorMessages);
274 |
275 | // now update the status of our files
276 | GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States);
277 |
278 | return InCommand.bCommandSuccessful;
279 | }
280 |
281 | bool FGitMarkForAddWorker::UpdateStates() const
282 | {
283 | return GitSourceControlUtils::UpdateCachedStates(States);
284 | }
285 |
286 | FName FGitDeleteWorker::GetName() const
287 | {
288 | return "Delete";
289 | }
290 |
291 | bool FGitDeleteWorker::Execute(FGitSourceControlCommand& InCommand)
292 | {
293 | check(InCommand.Operation->GetName() == GetName());
294 |
295 | InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("rm"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), InCommand.Files, InCommand.InfoMessages, InCommand.ErrorMessages);
296 |
297 | // now update the status of our files
298 | GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States);
299 |
300 | return InCommand.bCommandSuccessful;
301 | }
302 |
303 | bool FGitDeleteWorker::UpdateStates() const
304 | {
305 | return GitSourceControlUtils::UpdateCachedStates(States);
306 | }
307 |
308 |
309 | // Get lists of Missing files (ie "deleted"), Modified files, and "other than Added" Existing files
310 | void GetMissingVsExistingFiles(const TArray& InFiles, TArray& OutMissingFiles, TArray& OutAllExistingFiles, TArray& OutOtherThanAddedExistingFiles)
311 | {
312 | FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl");
313 | FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
314 |
315 | const TArray Files = (InFiles.Num() > 0) ? (InFiles) : (Provider.GetFilesInCache());
316 |
317 | TArray> LocalStates;
318 | Provider.GetState(Files, LocalStates, EStateCacheUsage::Use);
319 | for(const auto& State : LocalStates)
320 | {
321 | if(FPaths::FileExists(State->GetFilename()))
322 | {
323 | if(State->IsAdded())
324 | {
325 | OutAllExistingFiles.Add(State->GetFilename());
326 | }
327 | else if(State->IsModified())
328 | {
329 | OutOtherThanAddedExistingFiles.Add(State->GetFilename());
330 | OutAllExistingFiles.Add(State->GetFilename());
331 | }
332 | else if(State->CanRevert()) // for locked but unmodified files
333 | {
334 | OutOtherThanAddedExistingFiles.Add(State->GetFilename());
335 | }
336 | }
337 | else
338 | {
339 | if (State->IsSourceControlled())
340 | {
341 | OutMissingFiles.Add(State->GetFilename());
342 | }
343 | }
344 | }
345 | }
346 |
347 | FName FGitRevertWorker::GetName() const
348 | {
349 | return "Revert";
350 | }
351 |
352 | bool FGitRevertWorker::Execute(FGitSourceControlCommand& InCommand)
353 | {
354 | // Filter files by status to use the right "revert" commands on them
355 | TArray MissingFiles;
356 | TArray AllExistingFiles;
357 | TArray OtherThanAddedExistingFiles;
358 | GetMissingVsExistingFiles(InCommand.Files, MissingFiles, AllExistingFiles, OtherThanAddedExistingFiles);
359 |
360 | InCommand.bCommandSuccessful = true;
361 | if(MissingFiles.Num() > 0)
362 | {
363 | // "Added" files that have been deleted needs to be removed from source control
364 | InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("rm"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), MissingFiles, InCommand.InfoMessages, InCommand.ErrorMessages);
365 | }
366 | if(AllExistingFiles.Num() > 0)
367 | {
368 | // reset any changes already added to the index
369 | InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("reset"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), AllExistingFiles, InCommand.InfoMessages, InCommand.ErrorMessages);
370 | }
371 | if(OtherThanAddedExistingFiles.Num() > 0)
372 | {
373 | // revert any changes in working copy (this would fails if the asset was in "Added" state, since after "reset" it is now "untracked")
374 | InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("checkout"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), OtherThanAddedExistingFiles, InCommand.InfoMessages, InCommand.ErrorMessages);
375 | }
376 |
377 | if(InCommand.bUsingGitLfsLocking)
378 | {
379 | // unlock files: execute the LFS command on relative filenames
380 | // (unlock only locked files, that is, not Added files)
381 | const TArray LockedFiles = GetLockedFiles(OtherThanAddedExistingFiles);
382 | if(LockedFiles.Num() > 0)
383 | {
384 | const TArray RelativeFiles = GitSourceControlUtils::RelativeFilenames(LockedFiles, InCommand.PathToRepositoryRoot);
385 | for(const auto& RelativeFile : RelativeFiles)
386 | {
387 | TArray OneFile;
388 | OneFile.Add(RelativeFile);
389 | GitSourceControlUtils::RunCommand(TEXT("lfs unlock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), OneFile, InCommand.InfoMessages, InCommand.ErrorMessages);
390 | }
391 | }
392 | }
393 |
394 | // If no files were specified (full revert), refresh all relevant files instead of the specified files (which is an empty list in full revert)
395 | // This is required so that files that were "Marked for add" have their status updated after a full revert.
396 | TArray FilesToUpdate = InCommand.Files;
397 | if (InCommand.Files.Num() <= 0)
398 | {
399 | for (const auto& File : MissingFiles) FilesToUpdate.Add(File);
400 | for (const auto& File : AllExistingFiles) FilesToUpdate.Add(File);
401 | for (const auto& File : OtherThanAddedExistingFiles) FilesToUpdate.Add(File);
402 | }
403 |
404 | // now update the status of our files
405 | GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, FilesToUpdate, InCommand.ErrorMessages, States);
406 |
407 | return InCommand.bCommandSuccessful;
408 | }
409 |
410 | bool FGitRevertWorker::UpdateStates() const
411 | {
412 | return GitSourceControlUtils::UpdateCachedStates(States);
413 | }
414 |
415 | FName FGitSyncWorker::GetName() const
416 | {
417 | return "Sync";
418 | }
419 |
420 | bool FGitSyncWorker::Execute(FGitSourceControlCommand& InCommand)
421 | {
422 | // pull the branch to get remote changes by rebasing any local commits (not merging them to avoid complex graphs)
423 | TArray Parameters;
424 | Parameters.Add(TEXT("--rebase"));
425 | Parameters.Add(TEXT("--autostash"));
426 | // TODO Configure origin
427 | Parameters.Add(TEXT("origin"));
428 | Parameters.Add(TEXT("HEAD"));
429 | InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("pull"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters, TArray(), InCommand.InfoMessages, InCommand.ErrorMessages);
430 |
431 | // now update the status of our files
432 | GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States);
433 | GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary);
434 |
435 | return InCommand.bCommandSuccessful;
436 | }
437 |
438 | bool FGitSyncWorker::UpdateStates() const
439 | {
440 | return GitSourceControlUtils::UpdateCachedStates(States);
441 | }
442 |
443 |
444 | FName FGitPushWorker::GetName() const
445 | {
446 | return "Push";
447 | }
448 |
449 | bool FGitPushWorker::Execute(FGitSourceControlCommand& InCommand)
450 | {
451 |
452 | // If we have any locked files, check if we should unlock them
453 | TArray FilesToUnlock;
454 | if (InCommand.bUsingGitLfsLocking)
455 | {
456 | TMap Locks;
457 | // Get locks as relative paths
458 | GitSourceControlUtils::GetAllLocks(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, false, InCommand.ErrorMessages, Locks);
459 | if(Locks.Num() > 0)
460 | {
461 | // test to see what lfs files we would push, and compare to locked files, unlock after if push OK
462 | FString BranchName;
463 | GitSourceControlUtils::GetBranchName(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, BranchName);
464 |
465 | TArray LfsPushParameters;
466 | LfsPushParameters.Add(TEXT("push"));
467 | LfsPushParameters.Add(TEXT("--dry-run"));
468 | LfsPushParameters.Add(TEXT("origin"));
469 | LfsPushParameters.Add(BranchName);
470 | TArray LfsPushInfoMessages;
471 | TArray LfsPushErrMessages;
472 | InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("lfs"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, LfsPushParameters, TArray(), LfsPushInfoMessages, LfsPushErrMessages);
473 |
474 | if(InCommand.bCommandSuccessful)
475 | {
476 | // Result format is of the form
477 | // push f4ee401c063058a78842bb3ed98088e983c32aa447f346db54fa76f844a7e85e => Path/To/Asset.uasset
478 | // With some potential informationals we can ignore
479 | for (auto& Line : LfsPushInfoMessages)
480 | {
481 | if (Line.StartsWith(TEXT("push")))
482 | {
483 | FString Prefix, Filename;
484 | if (Line.Split(TEXT("=>"), &Prefix, &Filename))
485 | {
486 | Filename = Filename.TrimStartAndEnd();
487 | if (Locks.Contains(Filename))
488 | {
489 | // We do not need to check user or if the file has local modifications before attempting unlocking, git-lfs will reject the unlock if so
490 | // No point duplicating effort here
491 | FilesToUnlock.Add(Filename);
492 | UE_LOG(LogSourceControl, Log, TEXT("Post-push will try to unlock: %s"), *Filename);
493 | }
494 | }
495 | }
496 | }
497 | }
498 | }
499 |
500 | }
501 | // push the branch to its default remote
502 | // (works only if the default remote "origin" is set and does not require authentication)
503 | TArray Parameters;
504 | Parameters.Add(TEXT("--set-upstream"));
505 | // TODO Configure origin
506 | Parameters.Add(TEXT("origin"));
507 | Parameters.Add(TEXT("HEAD"));
508 | InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("push"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters, TArray(), InCommand.InfoMessages, InCommand.ErrorMessages);
509 |
510 | if(InCommand.bCommandSuccessful && InCommand.bUsingGitLfsLocking && FilesToUnlock.Num() > 0)
511 | {
512 | // unlock files: execute the LFS command on relative filenames
513 | for(const auto& FileToUnlock : FilesToUnlock)
514 | {
515 | TArray OneFile;
516 | OneFile.Add(FileToUnlock);
517 | bool bUnlocked = GitSourceControlUtils::RunCommand(TEXT("lfs unlock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), OneFile, InCommand.InfoMessages, InCommand.ErrorMessages);
518 | if (!bUnlocked)
519 | {
520 | // Report but don't fail, it's not essential
521 | UE_LOG(LogSourceControl, Log, TEXT("Unlock failed for %s"), *FileToUnlock);
522 | }
523 | }
524 |
525 | // We need to update status if we unlock
526 | // This command needs absolute filenames
527 | TArray AbsFilesToUnlock = GitSourceControlUtils::AbsoluteFilenames(FilesToUnlock, InCommand.PathToRepositoryRoot);
528 | GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, AbsFilesToUnlock, InCommand.ErrorMessages, States);
529 |
530 | }
531 |
532 | return InCommand.bCommandSuccessful;
533 | }
534 |
535 | bool FGitPushWorker::UpdateStates() const
536 | {
537 | return GitSourceControlUtils::UpdateCachedStates(States);
538 | }
539 |
540 | FName FGitUpdateStatusWorker::GetName() const
541 | {
542 | return "UpdateStatus";
543 | }
544 |
545 | bool FGitUpdateStatusWorker::Execute(FGitSourceControlCommand& InCommand)
546 | {
547 | check(InCommand.Operation->GetName() == GetName());
548 |
549 | TSharedRef Operation = StaticCastSharedRef(InCommand.Operation);
550 |
551 | if(InCommand.Files.Num() > 0)
552 | {
553 | InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States);
554 | GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository"));
555 |
556 | if(Operation->ShouldUpdateHistory())
557 | {
558 | for(int32 Index = 0; Index < States.Num(); Index++)
559 | {
560 | FString& File = InCommand.Files[Index];
561 | TGitSourceControlHistory History;
562 |
563 | if(States[Index].IsConflicted())
564 | {
565 | // In case of a merge conflict, we first need to get the tip of the "remote branch" (MERGE_HEAD)
566 | GitSourceControlUtils::RunGetHistory(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, File, true, InCommand.ErrorMessages, History);
567 | }
568 | // Get the history of the file in the current branch
569 | InCommand.bCommandSuccessful &= GitSourceControlUtils::RunGetHistory(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, File, false, InCommand.ErrorMessages, History);
570 | Histories.Add(*File, History);
571 | }
572 | }
573 | }
574 | else
575 | {
576 | // no path provided: only update the status of assets in Content/ directory and also Config files
577 | TArray ProjectDirs;
578 | ProjectDirs.Add(FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()));
579 | ProjectDirs.Add(FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir()));
580 | InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, ProjectDirs, InCommand.ErrorMessages, States);
581 | }
582 |
583 | GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary);
584 |
585 | // 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)
586 |
587 | return InCommand.bCommandSuccessful;
588 | }
589 |
590 | bool FGitUpdateStatusWorker::UpdateStates() const
591 | {
592 | bool bUpdated = GitSourceControlUtils::UpdateCachedStates(States);
593 |
594 | FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked( "GitSourceControl" );
595 | FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
596 |
597 | const FDateTime Now = FDateTime::Now();
598 |
599 | // add history, if any
600 | for(const auto& History : Histories)
601 | {
602 | TSharedRef State = Provider.GetStateInternal(History.Key);
603 | State->History = History.Value;
604 | State->TimeStamp = Now;
605 | bUpdated = true;
606 | }
607 |
608 | return bUpdated;
609 | }
610 |
611 | FName FGitCopyWorker::GetName() const
612 | {
613 | return "Copy";
614 | }
615 |
616 | bool FGitCopyWorker::Execute(FGitSourceControlCommand& InCommand)
617 | {
618 | check(InCommand.Operation->GetName() == GetName());
619 |
620 | // Copy or Move operation on a single file : Git does not need an explicit copy nor move,
621 | // but after a Move the Editor create a redirector file with the old asset name that points to the new asset.
622 | // The redirector needs to be commited with the new asset to perform a real rename.
623 | // => the following is to "MarkForAdd" the redirector, but it still need to be committed by selecting the whole directory and "check-in"
624 | InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), InCommand.Files, InCommand.InfoMessages, InCommand.ErrorMessages);
625 |
626 | return InCommand.bCommandSuccessful;
627 | }
628 |
629 | bool FGitCopyWorker::UpdateStates() const
630 | {
631 | return GitSourceControlUtils::UpdateCachedStates(States);
632 | }
633 |
634 | FName FGitResolveWorker::GetName() const
635 | {
636 | return "Resolve";
637 | }
638 |
639 | bool FGitResolveWorker::Execute( class FGitSourceControlCommand& InCommand )
640 | {
641 | check(InCommand.Operation->GetName() == GetName());
642 |
643 | // mark the conflicting files as resolved:
644 | TArray Results;
645 | InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), InCommand.Files, Results, InCommand.ErrorMessages);
646 |
647 | // now update the status of our files
648 | GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States);
649 |
650 | return InCommand.bCommandSuccessful;
651 | }
652 |
653 | bool FGitResolveWorker::UpdateStates() const
654 | {
655 | return GitSourceControlUtils::UpdateCachedStates(States);
656 | }
657 |
658 | #undef LOCTEXT_NAMESPACE
659 |
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlOperations.h:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 | //
3 | // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
4 | // or copy at http://opensource.org/licenses/MIT)
5 |
6 | #pragma once
7 |
8 | #include "CoreMinimal.h"
9 | #include "IGitSourceControlWorker.h"
10 | #include "GitSourceControlState.h"
11 |
12 | #include "ISourceControlOperation.h"
13 |
14 | /**
15 | * Internal operation used to push local commits to configured remote origin
16 | */
17 | class FGitPush : public ISourceControlOperation
18 | {
19 | public:
20 | // ISourceControlOperation interface
21 | virtual FName GetName() const override;
22 |
23 | virtual FText GetInProgressString() const override;
24 | };
25 |
26 | /** Called when first activated on a project, and then at project load time.
27 | * Look for the root directory of the git repository (where the ".git/" subdirectory is located). */
28 | class FGitConnectWorker : public IGitSourceControlWorker
29 | {
30 | public:
31 | virtual ~FGitConnectWorker() {}
32 | // IGitSourceControlWorker interface
33 | virtual FName GetName() const override;
34 | virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
35 | virtual bool UpdateStates() const override;
36 |
37 | public:
38 | /** Temporary states for results */
39 | TArray States;
40 | };
41 |
42 | /** Lock (check-out) a set of files using Git LFS 2. */
43 | class FGitCheckOutWorker : public IGitSourceControlWorker
44 | {
45 | public:
46 | virtual ~FGitCheckOutWorker() {}
47 | // IGitSourceControlWorker interface
48 | virtual FName GetName() const override;
49 | virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
50 | virtual bool UpdateStates() const override;
51 |
52 | public:
53 | /** Temporary states for results */
54 | TArray States;
55 | };
56 |
57 | /** Commit (check-in) a set of files to the local depot. */
58 | class FGitCheckInWorker : public IGitSourceControlWorker
59 | {
60 | public:
61 | virtual ~FGitCheckInWorker() {}
62 | // IGitSourceControlWorker interface
63 | virtual FName GetName() const override;
64 | virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
65 | virtual bool UpdateStates() const override;
66 |
67 | public:
68 | /** Temporary states for results */
69 | TArray States;
70 | };
71 |
72 | /** Add an untraked file to source control (so only a subset of the git add command). */
73 | class FGitMarkForAddWorker : public IGitSourceControlWorker
74 | {
75 | public:
76 | virtual ~FGitMarkForAddWorker() {}
77 | // IGitSourceControlWorker interface
78 | virtual FName GetName() const override;
79 | virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
80 | virtual bool UpdateStates() const override;
81 |
82 | public:
83 | /** Temporary states for results */
84 | TArray States;
85 | };
86 |
87 | /** Delete a file and remove it from source control. */
88 | class FGitDeleteWorker : public IGitSourceControlWorker
89 | {
90 | public:
91 | virtual ~FGitDeleteWorker() {}
92 | // IGitSourceControlWorker interface
93 | virtual FName GetName() const override;
94 | virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
95 | virtual bool UpdateStates() const override;
96 |
97 | public:
98 | /** Temporary states for results */
99 | TArray States;
100 | };
101 |
102 | /** Revert any change to a file to its state on the local depot. */
103 | class FGitRevertWorker : public IGitSourceControlWorker
104 | {
105 | public:
106 | virtual ~FGitRevertWorker() {}
107 | // IGitSourceControlWorker interface
108 | virtual FName GetName() const override;
109 | virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
110 | virtual bool UpdateStates() const override;
111 |
112 | public:
113 | /** Temporary states for results */
114 | TArray States;
115 | };
116 |
117 | /** Git pull --rebase to update branch from its configured remote */
118 | class FGitSyncWorker : public IGitSourceControlWorker
119 | {
120 | public:
121 | virtual ~FGitSyncWorker() {}
122 | // IGitSourceControlWorker interface
123 | virtual FName GetName() const override;
124 | virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
125 | virtual bool UpdateStates() const override;
126 |
127 | public:
128 | /** Temporary states for results */
129 | TArray States;
130 | };
131 |
132 | /** Git push to publish branch for its configured remote */
133 | class FGitPushWorker : public IGitSourceControlWorker
134 | {
135 | public:
136 | virtual ~FGitPushWorker() {}
137 | // IGitSourceControlWorker interface
138 | virtual FName GetName() const override;
139 | virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
140 | virtual bool UpdateStates() const override;
141 |
142 | public:
143 | /** Temporary states for results */
144 | TArray States;
145 | };
146 |
147 | /** Get source control status of files on local working copy. */
148 | class FGitUpdateStatusWorker : public IGitSourceControlWorker
149 | {
150 | public:
151 | virtual ~FGitUpdateStatusWorker() {}
152 | // IGitSourceControlWorker interface
153 | virtual FName GetName() const override;
154 | virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
155 | virtual bool UpdateStates() const override;
156 |
157 | public:
158 | /** Temporary states for results */
159 | TArray States;
160 |
161 | /** Map of filenames to history */
162 | TMap Histories;
163 | };
164 |
165 | /** Copy or Move operation on a single file */
166 | class FGitCopyWorker : public IGitSourceControlWorker
167 | {
168 | public:
169 | virtual ~FGitCopyWorker() {}
170 | // IGitSourceControlWorker interface
171 | virtual FName GetName() const override;
172 | virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
173 | virtual bool UpdateStates() const override;
174 |
175 | public:
176 | /** Temporary states for results */
177 | TArray States;
178 | };
179 |
180 | /** git add to mark a conflict as resolved */
181 | class FGitResolveWorker : public IGitSourceControlWorker
182 | {
183 | public:
184 | virtual ~FGitResolveWorker() {}
185 | virtual FName GetName() const override;
186 | virtual bool Execute(class FGitSourceControlCommand& InCommand) override;
187 | virtual bool UpdateStates() const override;
188 |
189 | private:
190 | /** Temporary states for results */
191 | TArray States;
192 | };
193 |
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlPrivatePCH.h:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 | //
3 | // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
4 | // or copy at http://opensource.org/licenses/MIT)
5 |
6 | #pragma once
7 |
8 | #include "CoreMinimal.h"
9 | #include "ISourceControlModule.h"
10 | #include "ISourceControlOperation.h"
11 | #include "ISourceControlProvider.h"
12 | #include "ISourceControlRevision.h"
13 | #include "ISourceControlState.h"
14 | #include "Misc/Paths.h"
15 | #include "Modules/ModuleManager.h"
16 |
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlProvider.cpp:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 | //
3 | // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
4 | // or copy at http://opensource.org/licenses/MIT)
5 |
6 | #include "GitSourceControlProvider.h"
7 |
8 | #include "HAL/PlatformProcess.h"
9 | #include "Misc/Paths.h"
10 | #include "Misc/QueuedThreadPool.h"
11 | #include "Modules/ModuleManager.h"
12 | #include "Widgets/DeclarativeSyntaxSupport.h"
13 | #include "GitSourceControlCommand.h"
14 | #include "ISourceControlModule.h"
15 | #include "GitSourceControlModule.h"
16 | #include "GitSourceControlUtils.h"
17 | #include "SGitSourceControlSettings.h"
18 | #include "Logging/MessageLog.h"
19 | #include "ScopedSourceControlProgress.h"
20 | #include "SourceControlHelpers.h"
21 | #include "SourceControlOperations.h"
22 | #include "Interfaces/IPluginManager.h"
23 |
24 | #define LOCTEXT_NAMESPACE "GitSourceControl"
25 |
26 | static FName ProviderName("Git LFS 2");
27 |
28 | void FGitSourceControlProvider::Init(bool bForceConnection)
29 | {
30 | // Init() is called multiple times at startup: do not check git each time
31 | if (!bGitAvailable)
32 | {
33 | const TSharedPtr Plugin = IPluginManager::Get().FindPlugin(TEXT("GitSourceControl"));
34 | if (Plugin.IsValid())
35 | {
36 | UE_LOG(LogSourceControl, Log, TEXT("Git plugin '%s'"), *(Plugin->GetDescriptor().VersionName));
37 | }
38 |
39 | CheckGitAvailability();
40 |
41 | const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl");
42 | bUsingGitLfsLocking = GitSourceControl.AccessSettings().IsUsingGitLfsLocking();
43 | }
44 |
45 | // bForceConnection: not used anymore
46 | }
47 |
48 | void FGitSourceControlProvider::CheckGitAvailability()
49 | {
50 | FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl");
51 | FString PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
52 | if (PathToGitBinary.IsEmpty())
53 | {
54 | // Try to find Git binary, and update settings accordingly
55 | PathToGitBinary = GitSourceControlUtils::FindGitBinaryPath();
56 | if (!PathToGitBinary.IsEmpty())
57 | {
58 | GitSourceControl.AccessSettings().SetBinaryPath(PathToGitBinary);
59 | }
60 | }
61 |
62 | if (!PathToGitBinary.IsEmpty())
63 | {
64 | UE_LOG(LogSourceControl, Log, TEXT("Using '%s'"), *PathToGitBinary);
65 | bGitAvailable = GitSourceControlUtils::CheckGitAvailability(PathToGitBinary, &GitVersion);
66 | if (bGitAvailable)
67 | {
68 | CheckRepositoryStatus(PathToGitBinary);
69 |
70 | // Register Console Commands (even without a workspace)
71 | GitSourceControlConsole.Register();
72 | }
73 | }
74 | else
75 | {
76 | bGitAvailable = false;
77 | }
78 | }
79 |
80 | void FGitSourceControlProvider::CheckRepositoryStatus(const FString& InPathToGitBinary)
81 | {
82 | // Find the path to the root Git directory (if any, else uses the ProjectDir)
83 | const FString PathToProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
84 | bGitRepositoryFound = GitSourceControlUtils::FindRootDirectory(PathToProjectDir, PathToRepositoryRoot);
85 | if (bGitRepositoryFound)
86 | {
87 | GitSourceControlMenu.Register();
88 |
89 | // Get branch name
90 | bGitRepositoryFound = GitSourceControlUtils::GetBranchName(InPathToGitBinary, PathToRepositoryRoot, BranchName);
91 | if (bGitRepositoryFound)
92 | {
93 | GitSourceControlUtils::GetRemoteUrl(InPathToGitBinary, PathToRepositoryRoot, RemoteUrl);
94 | }
95 | else
96 | {
97 | UE_LOG(LogSourceControl, Error, TEXT("'%s' is not a valid Git repository"), *PathToRepositoryRoot);
98 | }
99 | }
100 | else
101 | {
102 | UE_LOG(LogSourceControl, Warning, TEXT("'%s' is not part of a Git repository"), *FPaths::ProjectDir());
103 | }
104 |
105 | // Get user name & email (of the repository, else from the global Git config)
106 | GitSourceControlUtils::GetUserConfig(InPathToGitBinary, PathToRepositoryRoot, UserName, UserEmail);
107 | }
108 |
109 | void FGitSourceControlProvider::Close()
110 | {
111 | // clear the cache
112 | StateCache.Empty();
113 | // Remove all extensions to the "Source Control" menu in the Editor Toolbar
114 | GitSourceControlMenu.Unregister();
115 | // Unregister Console Commands
116 | GitSourceControlConsole.Unregister();
117 |
118 | bGitAvailable = false;
119 | bGitRepositoryFound = false;
120 | UserName.Empty();
121 | UserEmail.Empty();
122 | }
123 |
124 | TSharedRef FGitSourceControlProvider::GetStateInternal(const FString& Filename)
125 | {
126 | TSharedRef* State = StateCache.Find(Filename);
127 | if (State != NULL)
128 | {
129 | // found cached item
130 | return (*State);
131 | }
132 | else
133 | {
134 | // cache an unknown state for this item
135 | TSharedRef NewState = MakeShareable(new FGitSourceControlState(Filename, bUsingGitLfsLocking));
136 | StateCache.Add(Filename, NewState);
137 | return NewState;
138 | }
139 | }
140 |
141 | FText FGitSourceControlProvider::GetStatusText() const
142 | {
143 | FFormatNamedArguments Args;
144 | Args.Add(TEXT("RepositoryName"), FText::FromString(PathToRepositoryRoot));
145 | Args.Add(TEXT("RemoteUrl"), FText::FromString(RemoteUrl));
146 | Args.Add(TEXT("UserName"), FText::FromString(UserName));
147 | Args.Add(TEXT("UserEmail"), FText::FromString(UserEmail));
148 | Args.Add(TEXT("BranchName"), FText::FromString(BranchName));
149 | Args.Add(TEXT("CommitId"), FText::FromString(CommitId.Left(8)));
150 | Args.Add(TEXT("CommitSummary"), FText::FromString(CommitSummary));
151 |
152 | return FText::Format(NSLOCTEXT("Status", "Provider: Git\nEnabledLabel", "Local repository: {RepositoryName}\nRemote origin: {RemoteUrl}\nUser: {UserName}\nE-mail: {UserEmail}\n[{BranchName} {CommitId}] {CommitSummary}"), Args);
153 | }
154 |
155 | #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
156 |
157 | TMap FGitSourceControlProvider::GetStatus() const
158 | {
159 | TMap Result;
160 | Result.Add(EStatus::Enabled, IsEnabled() ? TEXT("Yes") : TEXT("No") );
161 | Result.Add(EStatus::Connected, (IsEnabled() && IsAvailable()) ? TEXT("Yes") : TEXT("No") );
162 | Result.Add(EStatus::User, UserName);
163 | Result.Add(EStatus::Repository, PathToRepositoryRoot);
164 | Result.Add(EStatus::Remote, RemoteUrl);
165 | Result.Add(EStatus::Branch, BranchName);
166 | Result.Add(EStatus::Email, UserEmail);
167 | return Result;
168 | }
169 |
170 | #endif
171 |
172 | /** Quick check if source control is enabled */
173 | bool FGitSourceControlProvider::IsEnabled() const
174 | {
175 | return bGitRepositoryFound;
176 | }
177 |
178 | /** Quick check if source control is available for use (useful for server-based providers) */
179 | bool FGitSourceControlProvider::IsAvailable() const
180 | {
181 | return bGitRepositoryFound;
182 | }
183 |
184 | const FName& FGitSourceControlProvider::GetName(void) const
185 | {
186 | return ProviderName;
187 | }
188 |
189 | ECommandResult::Type FGitSourceControlProvider::GetState(const TArray& InFiles, TArray>& OutState, EStateCacheUsage::Type InStateCacheUsage)
190 | {
191 | if (!IsEnabled())
192 | {
193 | return ECommandResult::Failed;
194 | }
195 |
196 | TArray AbsoluteFiles = SourceControlHelpers::AbsoluteFilenames(InFiles);
197 |
198 | if (InStateCacheUsage == EStateCacheUsage::ForceUpdate)
199 | {
200 | Execute(ISourceControlOperation::Create(), AbsoluteFiles);
201 | }
202 |
203 | for (const auto& AbsoluteFile : AbsoluteFiles)
204 | {
205 | OutState.Add(GetStateInternal(*AbsoluteFile));
206 | }
207 |
208 | return ECommandResult::Succeeded;
209 | }
210 |
211 | #if ENGINE_MAJOR_VERSION == 5
212 | ECommandResult::Type FGitSourceControlProvider::GetState(const TArray& InChangelists, TArray& OutState, EStateCacheUsage::Type InStateCacheUsage)
213 | {
214 | return ECommandResult::Failed;
215 | }
216 | #endif
217 |
218 | TArray FGitSourceControlProvider::GetCachedStateByPredicate(TFunctionRef Predicate) const
219 | {
220 | TArray Result;
221 | for (const auto& CacheItem : StateCache)
222 | {
223 | FSourceControlStateRef State = CacheItem.Value;
224 | if (Predicate(State))
225 | {
226 | Result.Add(State);
227 | }
228 | }
229 | return Result;
230 | }
231 |
232 | bool FGitSourceControlProvider::RemoveFileFromCache(const FString& Filename)
233 | {
234 | return StateCache.Remove(Filename) > 0;
235 | }
236 |
237 | /** Get files in cache */
238 | TArray FGitSourceControlProvider::GetFilesInCache()
239 | {
240 | TArray Files;
241 | for (const auto& State : StateCache)
242 | {
243 | Files.Add(State.Key);
244 | }
245 | return Files;
246 | }
247 |
248 | FDelegateHandle FGitSourceControlProvider::RegisterSourceControlStateChanged_Handle(const FSourceControlStateChanged::FDelegate& SourceControlStateChanged)
249 | {
250 | return OnSourceControlStateChanged.Add(SourceControlStateChanged);
251 | }
252 |
253 | void FGitSourceControlProvider::UnregisterSourceControlStateChanged_Handle(FDelegateHandle Handle)
254 | {
255 | OnSourceControlStateChanged.Remove(Handle);
256 | }
257 |
258 | #if ENGINE_MAJOR_VERSION == 5
259 | ECommandResult::Type FGitSourceControlProvider::Execute(const FSourceControlOperationRef& InOperation, FSourceControlChangelistPtr InChangelist, const TArray& InFiles, EConcurrency::Type InConcurrency /* = EConcurrency::Synchronous */, const FSourceControlOperationComplete& InOperationCompleteDelegate)
260 | #else
261 | ECommandResult::Type FGitSourceControlProvider::Execute(const TSharedRef& InOperation, const TArray& InFiles, EConcurrency::Type InConcurrency /* = EConcurrency::Synchronous */, const FSourceControlOperationComplete& InOperationCompleteDelegate)
262 | #endif
263 | {
264 | if (!IsEnabled() && !(InOperation->GetName() == "Connect")) // Only Connect operation allowed while not Enabled (Repository found)
265 | {
266 | InOperationCompleteDelegate.ExecuteIfBound(InOperation, ECommandResult::Failed);
267 | return ECommandResult::Failed;
268 | }
269 |
270 | TArray AbsoluteFiles = SourceControlHelpers::AbsoluteFilenames(InFiles);
271 |
272 | // Query to see if we allow this operation
273 | TSharedPtr Worker = CreateWorker(InOperation->GetName());
274 | if (!Worker.IsValid())
275 | {
276 | // this operation is unsupported by this source control provider
277 | FFormatNamedArguments Arguments;
278 | Arguments.Add(TEXT("OperationName"), FText::FromName(InOperation->GetName()));
279 | Arguments.Add(TEXT("ProviderName"), FText::FromName(GetName()));
280 | FText Message(FText::Format(LOCTEXT("UnsupportedOperation", "Operation '{OperationName}' not supported by source control provider '{ProviderName}'"), Arguments));
281 | FMessageLog("SourceControl").Error(Message);
282 | InOperation->AddErrorMessge(Message);
283 |
284 | InOperationCompleteDelegate.ExecuteIfBound(InOperation, ECommandResult::Failed);
285 | return ECommandResult::Failed;
286 | }
287 |
288 | FGitSourceControlCommand* Command = new FGitSourceControlCommand(InOperation, Worker.ToSharedRef());
289 | Command->Files = AbsoluteFiles;
290 | Command->OperationCompleteDelegate = InOperationCompleteDelegate;
291 |
292 | // fire off operation
293 | if (InConcurrency == EConcurrency::Synchronous)
294 | {
295 | Command->bAutoDelete = false;
296 |
297 | UE_LOG(LogSourceControl, Log, TEXT("ExecuteSynchronousCommand(%s)"), *InOperation->GetName().ToString());
298 | return ExecuteSynchronousCommand(*Command, InOperation->GetInProgressString());
299 | }
300 | else
301 | {
302 | Command->bAutoDelete = true;
303 |
304 | UE_LOG(LogSourceControl, Log, TEXT("IssueAsynchronousCommand(%s)"), *InOperation->GetName().ToString());
305 | return IssueCommand(*Command);
306 | }
307 | }
308 |
309 | bool FGitSourceControlProvider::CanExecuteOperation( const FSourceControlOperationRef& InOperation ) const
310 | {
311 | return WorkersMap.Find(InOperation->GetName()) != nullptr;
312 | }
313 |
314 | bool FGitSourceControlProvider::CanCancelOperation(const FSourceControlOperationRef& InOperation) const
315 | {
316 | return false;
317 | }
318 |
319 | void FGitSourceControlProvider::CancelOperation(const FSourceControlOperationRef& InOperation)
320 | {
321 | }
322 |
323 | bool FGitSourceControlProvider::UsesLocalReadOnlyState() const
324 | {
325 | return bUsingGitLfsLocking; // Git LFS Lock uses read-only state
326 | }
327 |
328 | bool FGitSourceControlProvider::UsesChangelists() const
329 | {
330 | return false;
331 | }
332 |
333 | bool FGitSourceControlProvider::UsesCheckout() const
334 | {
335 | return bUsingGitLfsLocking; // Git LFS Lock uses read-only state
336 | }
337 |
338 | /** Whether the provider uses individual file revisions. Used to enable partial 'Sync' operations on Content Browser Folders. */
339 | bool FGitSourceControlProvider::UsesFileRevisions() const
340 | {
341 | return false; // Partial 'Sync' doesn't make sense for Git, only for Perforce
342 | }
343 |
344 | /**
345 | * Whether the current source control client is at the latest version. Used to enable a global 'Sync' button on the Toolbar.
346 | * @note Experimental, hidden behind an setting in XxxEditor.ini [SourceControlSettings] DisplaySourceControlSyncStatus=true
347 | * @note This concept is currently only implemented for the Skein source control provider.
348 | */
349 | TOptional FGitSourceControlProvider::IsAtLatestRevision() const
350 | {
351 | return TOptional();
352 | }
353 |
354 | /**
355 | * Returns the number of changes in the local workspace. Used to enable a global 'CheckIn' button on the Toolbar.
356 | * @note Experimental, hidden behind an setting in XxxEditor.ini [SourceControlSettings] DisplaySourceControlCheckInStatus=true
357 | * @note This concept is currently only implemented for the Skein source control provider.
358 | */
359 | TOptional FGitSourceControlProvider::GetNumLocalChanges() const
360 | {
361 | return TOptional();
362 | }
363 |
364 | bool FGitSourceControlProvider::UsesUncontrolledChangelists() const
365 | {
366 | return true;
367 | }
368 |
369 | bool FGitSourceControlProvider::UsesSnapshots() const
370 | {
371 | return false;
372 | }
373 |
374 | bool FGitSourceControlProvider::AllowsDiffAgainstDepot() const
375 | {
376 | return true;
377 | }
378 |
379 | TSharedPtr FGitSourceControlProvider::CreateWorker(const FName& InOperationName) const
380 | {
381 | const FGetGitSourceControlWorker* Operation = WorkersMap.Find(InOperationName);
382 | if (Operation != nullptr)
383 | {
384 | return Operation->Execute();
385 | }
386 |
387 | return nullptr;
388 | }
389 |
390 | void FGitSourceControlProvider::RegisterWorker(const FName& InName, const FGetGitSourceControlWorker& InDelegate)
391 | {
392 | WorkersMap.Add(InName, InDelegate);
393 | }
394 |
395 | void FGitSourceControlProvider::OutputCommandMessages(const FGitSourceControlCommand& InCommand) const
396 | {
397 | FMessageLog SourceControlLog("SourceControl");
398 |
399 | for (int32 ErrorIndex = 0; ErrorIndex < InCommand.ErrorMessages.Num(); ++ErrorIndex)
400 | {
401 | SourceControlLog.Error(FText::FromString(InCommand.ErrorMessages[ErrorIndex]));
402 | }
403 |
404 | for (int32 InfoIndex = 0; InfoIndex < InCommand.InfoMessages.Num(); ++InfoIndex)
405 | {
406 | SourceControlLog.Info(FText::FromString(InCommand.InfoMessages[InfoIndex]));
407 | }
408 | }
409 |
410 | void FGitSourceControlProvider::UpdateRepositoryStatus(const class FGitSourceControlCommand& InCommand)
411 | {
412 | // For all operations running UpdateStatus, get Commit informations:
413 | if (!InCommand.CommitId.IsEmpty())
414 | {
415 | CommitId = InCommand.CommitId;
416 | CommitSummary = InCommand.CommitSummary;
417 | }
418 | }
419 |
420 | void FGitSourceControlProvider::Tick()
421 | {
422 | bool bStatesUpdated = false;
423 |
424 | for (int32 CommandIndex = 0; CommandIndex < CommandQueue.Num(); ++CommandIndex)
425 | {
426 | FGitSourceControlCommand& Command = *CommandQueue[CommandIndex];
427 | if (Command.bExecuteProcessed)
428 | {
429 | // Remove command from the queue
430 | CommandQueue.RemoveAt(CommandIndex);
431 |
432 | // Update respository status on UpdateStatus operations
433 | UpdateRepositoryStatus(Command);
434 |
435 | // let command update the states of any files
436 | bStatesUpdated |= Command.Worker->UpdateStates();
437 |
438 | // dump any messages to output log
439 | OutputCommandMessages(Command);
440 |
441 | // run the completion delegate callback if we have one bound
442 | Command.ReturnResults();
443 |
444 | // commands that are left in the array during a tick need to be deleted
445 | if (Command.bAutoDelete)
446 | {
447 | // Only delete commands that are not running 'synchronously'
448 | delete &Command;
449 | }
450 |
451 | // only do one command per tick loop, as we dont want concurrent modification
452 | // of the command queue (which can happen in the completion delegate)
453 | break;
454 | }
455 | }
456 |
457 | if (bStatesUpdated)
458 | {
459 | OnSourceControlStateChanged.Broadcast();
460 | }
461 | }
462 |
463 | TArray> FGitSourceControlProvider::GetLabels(const FString& InMatchingSpec) const
464 | {
465 | TArray> Tags;
466 |
467 | // NOTE list labels. Called by CrashDebugHelper() (to remote debug Engine crash)
468 | // and by SourceControlHelpers::AnnotateFile() (to add source file to report)
469 | // Reserved for internal use by Epic Games with Perforce only
470 | return Tags;
471 | }
472 |
473 | #if ENGINE_MAJOR_VERSION == 5
474 | TArray FGitSourceControlProvider::GetChangelists(EStateCacheUsage::Type InStateCacheUsage)
475 | {
476 | return TArray();
477 | }
478 | #endif
479 |
480 | #if SOURCE_CONTROL_WITH_SLATE
481 | TSharedRef FGitSourceControlProvider::MakeSettingsWidget() const
482 | {
483 | return SNew(SGitSourceControlSettings);
484 | }
485 | #endif
486 |
487 | ECommandResult::Type FGitSourceControlProvider::ExecuteSynchronousCommand(FGitSourceControlCommand& InCommand, const FText& Task)
488 | {
489 | ECommandResult::Type Result = ECommandResult::Failed;
490 |
491 | // Display the progress dialog if a string was provided
492 | {
493 | FScopedSourceControlProgress Progress(Task);
494 |
495 | // Issue the command asynchronously...
496 | IssueCommand(InCommand);
497 |
498 | // ... then wait for its completion (thus making it synchronous)
499 | while (!InCommand.bExecuteProcessed)
500 | {
501 | // Tick the command queue and update progress.
502 | Tick();
503 |
504 | Progress.Tick();
505 |
506 | // Sleep for a bit so we don't busy-wait so much.
507 | FPlatformProcess::Sleep(0.01f);
508 | }
509 |
510 | // always do one more Tick() to make sure the command queue is cleaned up.
511 | Tick();
512 |
513 | if (InCommand.bCommandSuccessful)
514 | {
515 | Result = ECommandResult::Succeeded;
516 | }
517 | }
518 |
519 | // Delete the command now (asynchronous commands are deleted in the Tick() method)
520 | check(!InCommand.bAutoDelete);
521 |
522 | // ensure commands that are not auto deleted do not end up in the command queue
523 | if (CommandQueue.Contains(&InCommand))
524 | {
525 | CommandQueue.Remove(&InCommand);
526 | }
527 | delete &InCommand;
528 |
529 | return Result;
530 | }
531 |
532 | ECommandResult::Type FGitSourceControlProvider::IssueCommand(FGitSourceControlCommand& InCommand)
533 | {
534 | if (GThreadPool != nullptr)
535 | {
536 | // Queue this to our worker thread(s) for resolving
537 | GThreadPool->AddQueuedWork(&InCommand);
538 | CommandQueue.Add(&InCommand);
539 | return ECommandResult::Succeeded;
540 | }
541 | else
542 | {
543 | FText Message(LOCTEXT("NoSCCThreads", "There are no threads available to process the source control command."));
544 |
545 | FMessageLog("SourceControl").Error(Message);
546 | InCommand.bCommandSuccessful = false;
547 | InCommand.Operation->AddErrorMessge(Message);
548 |
549 | return InCommand.ReturnResults();
550 | }
551 | }
552 |
553 | #undef LOCTEXT_NAMESPACE
554 |
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlProvider.h:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 | //
3 | // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
4 | // or copy at http://opensource.org/licenses/MIT)
5 |
6 | #pragma once
7 |
8 | #include "CoreMinimal.h"
9 | #include "ISourceControlOperation.h"
10 | #include "ISourceControlState.h"
11 | #include "ISourceControlProvider.h"
12 | #include "IGitSourceControlWorker.h"
13 | #include "GitSourceControlState.h"
14 | #include "GitSourceControlMenu.h"
15 | #include "GitSourceControlConsole.h"
16 |
17 | #include "Runtime/Launch/Resources/Version.h"
18 |
19 | class FGitSourceControlCommand;
20 |
21 | DECLARE_DELEGATE_RetVal(FGitSourceControlWorkerRef, FGetGitSourceControlWorker)
22 |
23 | /// Git version and capabilites extracted from the string "git version 2.11.0.windows.3"
24 | struct FGitVersion
25 | {
26 | // Git version extracted from the string "git version 2.11.0.windows.3" (Windows) or "git version 2.11.0" (Linux/Mac/Cygwin/WSL)
27 | int Major; // 2 Major version number
28 | int Minor; // 11 Minor version number
29 | int Patch; // 0 Patch/bugfix number
30 | int Windows; // 3 Windows specific revision number (under Windows only)
31 |
32 | uint32 bHasCatFileWithFilters : 1;
33 | uint32 bHasGitLfs : 1;
34 | uint32 bHasGitLfsLocking : 1;
35 |
36 | FGitVersion()
37 | : Major(0)
38 | , Minor(0)
39 | , Patch(0)
40 | , Windows(0)
41 | , bHasCatFileWithFilters(false)
42 | , bHasGitLfs(false)
43 | , bHasGitLfsLocking(false)
44 | {
45 | }
46 |
47 | inline bool IsGreaterOrEqualThan(int InMajor, int InMinor) const
48 | {
49 | return (Major > InMajor) || (Major == InMajor && (Minor >= InMinor));
50 | }
51 | };
52 |
53 | class FGitSourceControlProvider : public ISourceControlProvider
54 | {
55 | public:
56 | /** Constructor */
57 | FGitSourceControlProvider()
58 | {
59 | }
60 |
61 | /* ISourceControlProvider implementation */
62 | virtual void Init(bool bForceConnection = true) override;
63 | virtual void Close() override;
64 | virtual FText GetStatusText() const override;
65 | #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
66 | virtual TMap GetStatus() const override;
67 | #endif
68 | virtual bool IsEnabled() const override;
69 | virtual bool IsAvailable() const override;
70 | virtual const FName& GetName(void) const override;
71 | virtual bool QueryStateBranchConfig(const FString& ConfigSrc, const FString& ConfigDest) override { return false; }
72 | virtual void RegisterStateBranches(const TArray& BranchNames, const FString& ContentRoot) override {}
73 | virtual int32 GetStateBranchIndex(const FString& InBranchName) const override { return INDEX_NONE; }
74 | virtual ECommandResult::Type GetState(const TArray& InFiles, TArray& OutState, EStateCacheUsage::Type InStateCacheUsage) override;
75 | #if ENGINE_MAJOR_VERSION == 5
76 | virtual ECommandResult::Type GetState(const TArray& InChangelists, TArray& OutState, EStateCacheUsage::Type InStateCacheUsage) override;
77 | #endif
78 | virtual TArray GetCachedStateByPredicate(TFunctionRef Predicate) const override;
79 | virtual FDelegateHandle RegisterSourceControlStateChanged_Handle(const FSourceControlStateChanged::FDelegate& SourceControlStateChanged) override;
80 | virtual void UnregisterSourceControlStateChanged_Handle(FDelegateHandle Handle) override;
81 | #if ENGINE_MAJOR_VERSION == 5
82 | virtual ECommandResult::Type Execute(const FSourceControlOperationRef& InOperation, FSourceControlChangelistPtr InChangelist, const TArray& InFiles, EConcurrency::Type InConcurrency = EConcurrency::Synchronous, const FSourceControlOperationComplete& InOperationCompleteDelegate = FSourceControlOperationComplete() ) override;
83 | #else
84 | virtual ECommandResult::Type Execute(const FSourceControlOperationRef& InOperation, const TArray& InFiles, EConcurrency::Type InConcurrency = EConcurrency::Synchronous, const FSourceControlOperationComplete& InOperationCompleteDelegate = FSourceControlOperationComplete() ) override;
85 | #endif
86 | virtual bool CanExecuteOperation( const FSourceControlOperationRef& InOperation ) const; /* override NOTE: added in UE5.3 */
87 | virtual bool CanCancelOperation(const FSourceControlOperationRef& InOperation) const override;
88 | virtual void CancelOperation(const FSourceControlOperationRef& InOperation) override;
89 | virtual bool UsesLocalReadOnlyState() const override;
90 | virtual bool UsesChangelists() const override;
91 | virtual bool UsesCheckout() const override;
92 | virtual bool UsesFileRevisions() const; /* override NOTE: added in UE5.1 */
93 | virtual TOptional IsAtLatestRevision() const; /* override NOTE: added in UE5.1 */
94 | virtual TOptional GetNumLocalChanges() const; /* override NOTE: added in UE5.1 */
95 | virtual bool UsesUncontrolledChangelists() const; /* override NOTE: added in UE5.2 */
96 | virtual bool UsesSnapshots() const; /* override NOTE: added in UE5.2 */
97 | virtual bool AllowsDiffAgainstDepot() const; /* override NOTE: added in UE5.2 */
98 | virtual void Tick() override;
99 | virtual TArray< TSharedRef > GetLabels(const FString& InMatchingSpec) const override;
100 | #if ENGINE_MAJOR_VERSION == 5
101 | virtual TArray GetChangelists(EStateCacheUsage::Type InStateCacheUsage) override;
102 | #endif
103 | #if SOURCE_CONTROL_WITH_SLATE
104 | virtual TSharedRef MakeSettingsWidget() const override;
105 | #endif
106 |
107 | using ISourceControlProvider::Execute;
108 |
109 | /**
110 | * Check configuration, else standard paths, and run a Git "version" command to check the availability of the binary.
111 | */
112 | void CheckGitAvailability();
113 |
114 | /**
115 | * Find the .git/ repository and check it's status.
116 | */
117 | void CheckRepositoryStatus(const FString& InPathToGitBinary);
118 |
119 | /** Is git binary found and working. */
120 | inline bool IsGitAvailable() const
121 | {
122 | return bGitAvailable;
123 | }
124 |
125 | /** Git version for feature checking */
126 | inline const FGitVersion& GetGitVersion() const
127 | {
128 | return GitVersion;
129 | }
130 |
131 | /** Get the path to the root of the Git repository: can be the ProjectDir itself, or any parent directory */
132 | inline const FString& GetPathToRepositoryRoot() const
133 | {
134 | return PathToRepositoryRoot;
135 | }
136 |
137 | /** Git config user.name */
138 | inline const FString& GetUserName() const
139 | {
140 | return UserName;
141 | }
142 |
143 | /** Git config user.email */
144 | inline const FString& GetUserEmail() const
145 | {
146 | return UserEmail;
147 | }
148 |
149 | /** Git remote origin url */
150 | inline const FString& GetRemoteUrl() const
151 | {
152 | return RemoteUrl;
153 | }
154 |
155 | /** Helper function used to update state cache */
156 | TSharedRef GetStateInternal(const FString& Filename);
157 |
158 | /**
159 | * Register a worker with the provider.
160 | * This is used internally so the provider can maintain a map of all available operations.
161 | */
162 | void RegisterWorker( const FName& InName, const FGetGitSourceControlWorker& InDelegate );
163 |
164 | /** Remove a named file from the state cache */
165 | bool RemoveFileFromCache(const FString& Filename);
166 |
167 | /** Get files in cache */
168 | TArray GetFilesInCache();
169 |
170 | private:
171 |
172 | /** Is git binary found and working. */
173 | bool bGitAvailable = false;
174 |
175 | /** Is git repository found. */
176 | bool bGitRepositoryFound = false;
177 |
178 | /** Is LFS File Locking enabled? */
179 | bool bUsingGitLfsLocking = false;
180 |
181 | /** Helper function for Execute() */
182 | TSharedPtr CreateWorker(const FName& InOperationName) const;
183 |
184 | /** Helper function for running command synchronously. */
185 | ECommandResult::Type ExecuteSynchronousCommand(class FGitSourceControlCommand& InCommand, const FText& Task);
186 | /** Issue a command asynchronously if possible. */
187 | ECommandResult::Type IssueCommand(class FGitSourceControlCommand& InCommand);
188 |
189 | /** Output any messages this command holds */
190 | void OutputCommandMessages(const class FGitSourceControlCommand& InCommand) const;
191 |
192 | /** Update repository status on Connect and UpdateStatus operations */
193 | void UpdateRepositoryStatus(const class FGitSourceControlCommand& InCommand);
194 |
195 | /** Path to the root of the Git repository: can be the ProjectDir itself, or any parent directory (found by the "Connect" operation) */
196 | FString PathToRepositoryRoot;
197 |
198 | /** Git config user.name (from local repository, else globally) */
199 | FString UserName;
200 |
201 | /** Git config user.email (from local repository, else globally) */
202 | FString UserEmail;
203 |
204 | /** Name of the current branch */
205 | FString BranchName;
206 |
207 | /** URL of the "origin" defaut remote server */
208 | FString RemoteUrl;
209 |
210 | /** Current Commit full SHA1 */
211 | FString CommitId;
212 |
213 | /** Current Commit description's Summary */
214 | FString CommitSummary;
215 |
216 | /** State cache */
217 | TMap > StateCache;
218 |
219 | /** The currently registered source control operations */
220 | TMap WorkersMap;
221 |
222 | /** Queue for commands given by the main thread */
223 | TArray < FGitSourceControlCommand* > CommandQueue;
224 |
225 | /** For notifying when the source control states in the cache have changed */
226 | FSourceControlStateChanged OnSourceControlStateChanged;
227 |
228 | /** Git version for feature checking */
229 | FGitVersion GitVersion;
230 |
231 | /** Source Control Menu Extension */
232 | FGitSourceControlMenu GitSourceControlMenu;
233 |
234 | /** Source Control Console commands */
235 | FGitSourceControlConsole GitSourceControlConsole;
236 | };
237 |
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlRevision.cpp:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 | //
3 | // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
4 | // or copy at http://opensource.org/licenses/MIT)
5 |
6 | #include "GitSourceControlRevision.h"
7 |
8 | #include "HAL/FileManager.h"
9 | #include "Misc/Paths.h"
10 | #include "Modules/ModuleManager.h"
11 | #include "GitSourceControlModule.h"
12 | #include "GitSourceControlProvider.h"
13 | #include "GitSourceControlUtils.h"
14 |
15 | #define LOCTEXT_NAMESPACE "GitSourceControl"
16 |
17 | #if ENGINE_MAJOR_VERSION == 5
18 | bool FGitSourceControlRevision::Get(FString& InOutFilename, EConcurrency::Type InConcurrency /* = EConcurrency::Synchronous */) const
19 | #else
20 | bool FGitSourceControlRevision::Get(FString& InOutFilename) const
21 | #endif
22 | {
23 | #if ENGINE_MAJOR_VERSION == 5
24 | if (InConcurrency != EConcurrency::Synchronous)
25 | {
26 | UE_LOG(LogSourceControl, Warning, TEXT("Only EConcurrency::Synchronous is tested/supported for this operation."));
27 | }
28 | #endif
29 |
30 | FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl");
31 | const FString PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
32 | const FString PathToRepositoryRoot = GitSourceControl.GetProvider().GetPathToRepositoryRoot();
33 |
34 | // if a filename for the temp file wasn't supplied generate a unique-ish one
35 | if(InOutFilename.Len() == 0)
36 | {
37 | // create the diff dir if we don't already have it (Git wont)
38 | IFileManager::Get().MakeDirectory(*FPaths::DiffDir(), true);
39 | // create a unique temp file name based on the unique commit Id
40 | const FString TempFileName = FString::Printf(TEXT("%stemp-%s-%s"), *FPaths::DiffDir(), *CommitId, *FPaths::GetCleanFilename(Filename));
41 | InOutFilename = FPaths::ConvertRelativePathToFull(TempFileName);
42 | }
43 |
44 | // Diff against the revision
45 | const FString Parameter = FString::Printf(TEXT("%s:%s"), *CommitId, *Filename);
46 |
47 | bool bCommandSuccessful;
48 | if(FPaths::FileExists(InOutFilename))
49 | {
50 | bCommandSuccessful = true; // if the temp file already exists, reuse it directly
51 | }
52 | else
53 | {
54 | bCommandSuccessful = GitSourceControlUtils::RunDumpToFile(PathToGitBinary, PathToRepositoryRoot, Parameter, InOutFilename);
55 | }
56 | return bCommandSuccessful;
57 | }
58 |
59 | bool FGitSourceControlRevision::GetAnnotated( TArray& OutLines ) const
60 | {
61 | return false;
62 | }
63 |
64 | bool FGitSourceControlRevision::GetAnnotated( FString& InOutFilename ) const
65 | {
66 | return false;
67 | }
68 |
69 | const FString& FGitSourceControlRevision::GetFilename() const
70 | {
71 | return Filename;
72 | }
73 |
74 | int32 FGitSourceControlRevision::GetRevisionNumber() const
75 | {
76 | return RevisionNumber;
77 | }
78 |
79 | const FString& FGitSourceControlRevision::GetRevision() const
80 | {
81 | return ShortCommitId;
82 | }
83 |
84 | const FString& FGitSourceControlRevision::GetDescription() const
85 | {
86 | return Description;
87 | }
88 |
89 | const FString& FGitSourceControlRevision::GetUserName() const
90 | {
91 | return UserName;
92 | }
93 |
94 | const FString& FGitSourceControlRevision::GetClientSpec() const
95 | {
96 | static FString EmptyString(TEXT(""));
97 | return EmptyString;
98 | }
99 |
100 | const FString& FGitSourceControlRevision::GetAction() const
101 | {
102 | return Action;
103 | }
104 |
105 | TSharedPtr FGitSourceControlRevision::GetBranchSource() const
106 | {
107 | // if this revision was copied/moved from some other revision
108 | return BranchSource;
109 | }
110 |
111 | const FDateTime& FGitSourceControlRevision::GetDate() const
112 | {
113 | return Date;
114 | }
115 |
116 | int32 FGitSourceControlRevision::GetCheckInIdentifier() const
117 | {
118 | return CommitIdNumber;
119 | }
120 |
121 | int32 FGitSourceControlRevision::GetFileSize() const
122 | {
123 | return FileSize;
124 | }
125 |
126 | #undef LOCTEXT_NAMESPACE
127 |
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlRevision.h:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 | //
3 | // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
4 | // or copy at http://opensource.org/licenses/MIT)
5 |
6 | #pragma once
7 |
8 | #include "CoreMinimal.h"
9 | #include "ISourceControlRevision.h"
10 |
11 | #include "Runtime/Launch/Resources/Version.h"
12 |
13 | /** Revision of a file, linked to a specific commit */
14 | class FGitSourceControlRevision : public ISourceControlRevision
15 | {
16 | public:
17 |
18 | /** ISourceControlRevision interface */
19 | #if ENGINE_MAJOR_VERSION == 5
20 | virtual bool Get(FString& InOutFilename, EConcurrency::Type InConcurrency = EConcurrency::Synchronous) const override;
21 | #else
22 | virtual bool Get(FString& InOutFilename) const override;
23 | #endif
24 | virtual bool GetAnnotated(TArray& OutLines) const override;
25 | virtual bool GetAnnotated(FString& InOutFilename) const override;
26 | virtual const FString& GetFilename() const override;
27 | virtual int32 GetRevisionNumber() const override;
28 | virtual const FString& GetRevision() const override;
29 | virtual const FString& GetDescription() const override;
30 | virtual const FString& GetUserName() const override;
31 | virtual const FString& GetClientSpec() const override;
32 | virtual const FString& GetAction() const override;
33 | virtual TSharedPtr GetBranchSource() const override;
34 | virtual const FDateTime& GetDate() const override;
35 | virtual int32 GetCheckInIdentifier() const override;
36 | virtual int32 GetFileSize() const override;
37 |
38 | public:
39 |
40 | /** The filename this revision refers to */
41 | FString Filename;
42 |
43 | /** The full hexadecimal SHA1 id of the commit this revision refers to */
44 | FString CommitId;
45 |
46 | /** The short hexadecimal SHA1 id (8 first hex char out of 40) of the commit: the string to display */
47 | FString ShortCommitId;
48 |
49 | /** The numeric value of the short SHA1 (8 first hex char out of 40) */
50 | int32 CommitIdNumber = 0;
51 |
52 | /** The index of the revision in the history (SBlueprintRevisionMenu assumes order for the "Depot" label) */
53 | int32 RevisionNumber = 0;
54 |
55 | /** The SHA1 identifier of the file at this revision */
56 | FString FileHash;
57 |
58 | /** The description of this revision */
59 | FString Description;
60 |
61 | /** The user that made the change */
62 | FString UserName;
63 |
64 | /** The action (add, edit, branch etc.) performed at this revision */
65 | FString Action;
66 |
67 | /** Source of move ("branch" in Perforce term) if any */
68 | TSharedPtr BranchSource;
69 |
70 | /** The date this revision was made */
71 | FDateTime Date;
72 |
73 | /** The size of the file at this revision */
74 | int32 FileSize;
75 | };
76 |
77 | /** History composed of the last 100 revisions of the file */
78 | typedef TArray< TSharedRef > TGitSourceControlHistory;
79 |
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlSettings.cpp:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 | //
3 | // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
4 | // or copy at http://opensource.org/licenses/MIT)
5 |
6 | #include "GitSourceControlSettings.h"
7 |
8 | #include "Misc/ScopeLock.h"
9 | #include "Misc/ConfigCacheIni.h"
10 | #include "Modules/ModuleManager.h"
11 | #include "SourceControlHelpers.h"
12 | #include "GitSourceControlModule.h"
13 | #include "GitSourceControlUtils.h"
14 |
15 | namespace GitSettingsConstants
16 | {
17 |
18 | /** The section of the ini file we load our settings from */
19 | static const FString SettingsSection = TEXT("GitSourceControl.GitSourceControlSettings");
20 |
21 | }
22 |
23 | const FString FGitSourceControlSettings::GetBinaryPath() const
24 | {
25 | FScopeLock ScopeLock(&CriticalSection);
26 | return BinaryPath; // Return a copy to be thread-safe
27 | }
28 |
29 | bool FGitSourceControlSettings::SetBinaryPath(const FString& InString)
30 | {
31 | FScopeLock ScopeLock(&CriticalSection);
32 | const bool bChanged = (BinaryPath != InString);
33 | if(bChanged)
34 | {
35 | BinaryPath = InString;
36 | }
37 | return bChanged;
38 | }
39 |
40 | /** Tell if using the Git LFS file Locking workflow */
41 | bool FGitSourceControlSettings::IsUsingGitLfsLocking() const
42 | {
43 | FScopeLock ScopeLock(&CriticalSection);
44 | return bUsingGitLfsLocking;
45 | }
46 |
47 | /** Configure the usage of Git LFS file Locking workflow */
48 | bool FGitSourceControlSettings::SetUsingGitLfsLocking(const bool InUsingGitLfsLocking)
49 | {
50 | FScopeLock ScopeLock(&CriticalSection);
51 | const bool bChanged = (bUsingGitLfsLocking != InUsingGitLfsLocking);
52 | bUsingGitLfsLocking = InUsingGitLfsLocking;
53 | return bChanged;
54 | }
55 |
56 | const FString FGitSourceControlSettings::GetLfsUserName() const
57 | {
58 | FScopeLock ScopeLock(&CriticalSection);
59 | return LfsUserName; // Return a copy to be thread-safe
60 | }
61 |
62 | bool FGitSourceControlSettings::SetLfsUserName(const FString& InString)
63 | {
64 | FScopeLock ScopeLock(&CriticalSection);
65 | const bool bChanged = (LfsUserName != InString);
66 | if (bChanged)
67 | {
68 | LfsUserName = InString;
69 | }
70 | return bChanged;
71 | }
72 |
73 | bool FGitSourceControlSettings::SetIsPushAfterCommitEnabled(bool bInEnabled)
74 | {
75 | FScopeLock ScopeLock(&CriticalSection);
76 | const bool bChanged = (bIsPushAfterCommitEnabled != bInEnabled);
77 | if (bChanged)
78 | {
79 | bIsPushAfterCommitEnabled = bInEnabled;
80 | }
81 | return bChanged;
82 | }
83 |
84 | bool FGitSourceControlSettings::IsPushAfterCommitEnabled() const
85 | {
86 | FScopeLock ScopeLock(&CriticalSection);
87 | return bIsPushAfterCommitEnabled;
88 | }
89 |
90 | // This is called at startup nearly before anything else in our module: BinaryPath will then be used by the provider
91 | void FGitSourceControlSettings::LoadSettings()
92 | {
93 | FScopeLock ScopeLock(&CriticalSection);
94 | const FString& IniFile = SourceControlHelpers::GetSettingsIni();
95 | GConfig->GetString(*GitSettingsConstants::SettingsSection, TEXT("BinaryPath"), BinaryPath, IniFile);
96 | GConfig->GetBool(*GitSettingsConstants::SettingsSection, TEXT("UsingGitLfsLocking"), bUsingGitLfsLocking, IniFile);
97 | GConfig->GetString(*GitSettingsConstants::SettingsSection, TEXT("LfsUserName"), LfsUserName, IniFile);
98 | GConfig->GetBool(*GitSettingsConstants::SettingsSection, TEXT("IsPushAfterCommitEnabled"), bIsPushAfterCommitEnabled, IniFile);
99 | }
100 |
101 | void FGitSourceControlSettings::SaveSettings() const
102 | {
103 | FScopeLock ScopeLock(&CriticalSection);
104 | const FString& IniFile = SourceControlHelpers::GetSettingsIni();
105 | GConfig->SetString(*GitSettingsConstants::SettingsSection, TEXT("BinaryPath"), *BinaryPath, IniFile);
106 | GConfig->SetBool(*GitSettingsConstants::SettingsSection, TEXT("UsingGitLfsLocking"), bUsingGitLfsLocking, IniFile);
107 | GConfig->SetString(*GitSettingsConstants::SettingsSection, TEXT("LfsUserName"), *LfsUserName, IniFile);
108 | GConfig->SetBool(*GitSettingsConstants::SettingsSection, TEXT("IsPushAfterCommitEnabled"), bIsPushAfterCommitEnabled, IniFile);
109 | }
110 |
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlSettings.h:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 | //
3 | // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
4 | // or copy at http://opensource.org/licenses/MIT)
5 |
6 | #pragma once
7 |
8 | #include "CoreMinimal.h"
9 |
10 | class FGitSourceControlSettings
11 | {
12 | public:
13 | /** Get the Git Binary Path */
14 | const FString GetBinaryPath() const;
15 |
16 | /** Set the Git Binary Path */
17 | bool SetBinaryPath(const FString& InString);
18 |
19 | /** Tell if using the Git LFS file Locking workflow */
20 | bool IsUsingGitLfsLocking() const;
21 |
22 | /** Configure the usage of Git LFS file Locking workflow */
23 | bool SetUsingGitLfsLocking(const bool InUsingGitLfsLocking);
24 |
25 | /** Get the username used by the Git LFS 2 File Locks server */
26 | const FString GetLfsUserName() const;
27 |
28 | /** Set the username used by the Git LFS 2 File Locks server */
29 | bool SetLfsUserName(const FString& InString);
30 |
31 | /** Set whether Submit means Commit AND push (default true) */
32 | bool SetIsPushAfterCommitEnabled(bool bCond);
33 |
34 | /** Get whether Submit means Commit AND push (default true) */
35 | bool IsPushAfterCommitEnabled() const;
36 |
37 | /** Load settings from ini file */
38 | void LoadSettings();
39 |
40 | /** Save settings to ini file */
41 | void SaveSettings() const;
42 |
43 | private:
44 | /** A critical section for settings access */
45 | mutable FCriticalSection CriticalSection;
46 |
47 | /** Git binary path */
48 | FString BinaryPath;
49 |
50 | /** Tells if using the Git LFS file Locking workflow */
51 | bool bUsingGitLfsLocking;
52 |
53 | /** Username used by the Git LFS 2 File Locks server */
54 | FString LfsUserName;
55 |
56 | /** Does Submit mean Commit AND push */
57 | bool bIsPushAfterCommitEnabled = true;
58 | };
59 |
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlState.cpp:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 | //
3 | // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
4 | // or copy at http://opensource.org/licenses/MIT)
5 |
6 | #include "GitSourceControlState.h"
7 | #if ENGINE_MAJOR_VERSION == 5
8 | #include "Styling/AppStyle.h"
9 | #endif
10 |
11 | #define LOCTEXT_NAMESPACE "GitSourceControl.State"
12 |
13 | int32 FGitSourceControlState::GetHistorySize() const
14 | {
15 | return History.Num();
16 | }
17 |
18 | TSharedPtr FGitSourceControlState::GetHistoryItem(int32 HistoryIndex) const
19 | {
20 | check(History.IsValidIndex(HistoryIndex));
21 | return History[HistoryIndex];
22 | }
23 |
24 | TSharedPtr FGitSourceControlState::FindHistoryRevision(int32 RevisionNumber) const
25 | {
26 | for (const auto& Revision : History)
27 | {
28 | if (Revision->GetRevisionNumber() == RevisionNumber)
29 | {
30 | return Revision;
31 | }
32 | }
33 |
34 | return nullptr;
35 | }
36 |
37 | TSharedPtr FGitSourceControlState::FindHistoryRevision(const FString& InRevision) const
38 | {
39 | for (const auto& Revision : History)
40 | {
41 | if (Revision->GetRevision() == InRevision)
42 | {
43 | return Revision;
44 | }
45 | }
46 |
47 | return nullptr;
48 | }
49 |
50 | #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
51 |
52 | ISourceControlState::FResolveInfo FGitSourceControlState::GetResolveInfo() const
53 | {
54 | return PendingResolveInfo;
55 | }
56 |
57 | #else
58 |
59 | TSharedPtr FGitSourceControlState::GetBaseRevForMerge() const
60 | {
61 | for (const auto& Revision : History)
62 | {
63 | // look for the the SHA1 id of the file, not the commit id (revision)
64 | if (Revision->FileHash == PendingMergeBaseFileHash)
65 | {
66 | return Revision;
67 | }
68 | }
69 |
70 | return nullptr;
71 | }
72 |
73 | #endif
74 |
75 | #if ENGINE_MAJOR_VERSION == 5
76 |
77 | FSlateIcon FGitSourceControlState::GetIcon() const
78 | {
79 | switch (WorkingCopyState)
80 | {
81 | case EWorkingCopyState::Modified:
82 | return FSlateIcon(FAppStyle::GetAppStyleSetName(), "Subversion.CheckedOut");
83 | case EWorkingCopyState::Added:
84 | return FSlateIcon(FAppStyle::GetAppStyleSetName(), "Subversion.OpenForAdd");
85 | case EWorkingCopyState::Renamed:
86 | case EWorkingCopyState::Copied:
87 | return FSlateIcon(FAppStyle::GetAppStyleSetName(), "Subversion.Branched");
88 | case EWorkingCopyState::Deleted: // Deleted & Missing files does not show in Content Browser
89 | case EWorkingCopyState::Missing:
90 | return FSlateIcon(FAppStyle::GetAppStyleSetName(), "Subversion.MarkedForDelete");
91 | case EWorkingCopyState::Conflicted:
92 | return FSlateIcon(FAppStyle::GetAppStyleSetName(), "Subversion.NotAtHeadRevision");
93 | case EWorkingCopyState::NotControlled:
94 | return FSlateIcon(FAppStyle::GetAppStyleSetName(), "Subversion.NotInDepot");
95 | case EWorkingCopyState::Unknown:
96 | case EWorkingCopyState::Unchanged: // Unchanged is the same as "Pristine" (not checked out) for Perforce, ie no icon
97 | case EWorkingCopyState::Ignored:
98 | default:
99 | return FSlateIcon();
100 | }
101 |
102 | return FSlateIcon();
103 | }
104 |
105 | #else
106 |
107 | // @todo add Slate icons for git specific states (NotAtHead vs Conflicted...)
108 | FName FGitSourceControlState::GetIconName() const
109 | {
110 | if(LockState == ELockState::Locked)
111 | {
112 | return FName("Subversion.CheckedOut");
113 | }
114 | else if(LockState == ELockState::LockedOther)
115 | {
116 | return FName("Subversion.CheckedOutByOtherUser");
117 | }
118 | else if (!IsCurrent())
119 | {
120 | return FName("Subversion.NotAtHeadRevision");
121 | }
122 |
123 | switch(WorkingCopyState)
124 | {
125 | case EWorkingCopyState::Modified:
126 | if(bUsingGitLfsLocking)
127 | {
128 | return FName("Subversion.NotInDepot");
129 | }
130 | else
131 | {
132 | return FName("Subversion.CheckedOut");
133 | }
134 | case EWorkingCopyState::Added:
135 | return FName("Subversion.OpenForAdd");
136 | case EWorkingCopyState::Renamed:
137 | case EWorkingCopyState::Copied:
138 | return FName("Subversion.Branched");
139 | case EWorkingCopyState::Deleted: // Deleted & Missing files does not show in Content Browser
140 | case EWorkingCopyState::Missing:
141 | return FName("Subversion.MarkedForDelete");
142 | case EWorkingCopyState::Conflicted:
143 | return FName("Subversion.ModifiedOtherBranch");
144 | case EWorkingCopyState::NotControlled:
145 | return FName("Subversion.NotInDepot");
146 | case EWorkingCopyState::Unknown:
147 | case EWorkingCopyState::Unchanged: // Unchanged is the same as "Pristine" (not checked out) for Perforce, ie no icon
148 | case EWorkingCopyState::Ignored:
149 | default:
150 | return NAME_None;
151 | }
152 |
153 | return NAME_None;
154 | }
155 |
156 | FName FGitSourceControlState::GetSmallIconName() const
157 | {
158 | if(LockState == ELockState::Locked)
159 | {
160 | return FName("Subversion.CheckedOut_Small");
161 | }
162 | else if(LockState == ELockState::LockedOther)
163 | {
164 | return FName("Subversion.CheckedOutByOtherUser_Small");
165 | }
166 | else if (!IsCurrent())
167 | {
168 | return FName("Subversion.NotAtHeadRevision_Small");
169 | }
170 |
171 | switch(WorkingCopyState)
172 | {
173 | case EWorkingCopyState::Modified:
174 | if(bUsingGitLfsLocking)
175 | {
176 | return FName("Subversion.NotInDepot_Small");
177 | }
178 | else
179 | {
180 | return FName("Subversion.CheckedOut_Small");
181 | }
182 | case EWorkingCopyState::Added:
183 | return FName("Subversion.OpenForAdd_Small");
184 | case EWorkingCopyState::Renamed:
185 | case EWorkingCopyState::Copied:
186 | return FName("Subversion.Branched_Small");
187 | case EWorkingCopyState::Deleted: // Deleted & Missing files can appear in the Submit to Source Control window
188 | case EWorkingCopyState::Missing:
189 | return FName("Subversion.MarkedForDelete_Small");
190 | case EWorkingCopyState::Conflicted:
191 | return FName("Subversion.ModifiedOtherBranch_Small");
192 | case EWorkingCopyState::NotControlled:
193 | return FName("Subversion.NotInDepot_Small");
194 | case EWorkingCopyState::Unknown:
195 | case EWorkingCopyState::Unchanged: // Unchanged is the same as "Pristine" (not checked out) for Perforce, ie no icon
196 | case EWorkingCopyState::Ignored:
197 | default:
198 | return NAME_None;
199 | }
200 |
201 | return NAME_None;
202 | }
203 |
204 | #endif
205 |
206 | FText FGitSourceControlState::GetDisplayName() const
207 | {
208 | if (LockState == ELockState::Locked)
209 | {
210 | return LOCTEXT("Locked", "Locked For Editing");
211 | }
212 | else if (LockState == ELockState::LockedOther)
213 | {
214 | return FText::Format(LOCTEXT("LockedOther", "Locked by "), FText::FromString(LockUser));
215 | }
216 | else if (!IsCurrent())
217 | {
218 | return LOCTEXT("NotCurrent", "Not current");
219 | }
220 |
221 | switch (WorkingCopyState)
222 | {
223 | case EWorkingCopyState::Unknown:
224 | return LOCTEXT("Unknown", "Unknown");
225 | case EWorkingCopyState::Unchanged:
226 | return LOCTEXT("Unchanged", "Unchanged");
227 | case EWorkingCopyState::Added:
228 | return LOCTEXT("Added", "Added");
229 | case EWorkingCopyState::Deleted:
230 | return LOCTEXT("Deleted", "Deleted");
231 | case EWorkingCopyState::Modified:
232 | return LOCTEXT("Modified", "Modified");
233 | case EWorkingCopyState::Renamed:
234 | return LOCTEXT("Renamed", "Renamed");
235 | case EWorkingCopyState::Copied:
236 | return LOCTEXT("Copied", "Copied");
237 | case EWorkingCopyState::Conflicted:
238 | return LOCTEXT("ContentsConflict", "Contents Conflict");
239 | case EWorkingCopyState::Ignored:
240 | return LOCTEXT("Ignored", "Ignored");
241 | case EWorkingCopyState::NotControlled:
242 | return LOCTEXT("NotControlled", "Not Under Source Control");
243 | case EWorkingCopyState::Missing:
244 | return LOCTEXT("Missing", "Missing");
245 | }
246 |
247 | return FText();
248 | }
249 |
250 | FText FGitSourceControlState::GetDisplayTooltip() const
251 | {
252 | if (LockState == ELockState::Locked)
253 | {
254 | return LOCTEXT("Locked_Tooltip", "Locked for editing by current user");
255 | }
256 | else if (LockState == ELockState::LockedOther)
257 | {
258 | return FText::Format(LOCTEXT("LockedOther_Tooltip", "Locked for editing by: {0}"), FText::FromString(LockUser));
259 | }
260 | else if (!IsCurrent())
261 | {
262 | return LOCTEXT("NotCurrent_Tooltip", "The file(s) are not at the head revision");
263 | }
264 |
265 | switch (WorkingCopyState)
266 | {
267 | case EWorkingCopyState::Unknown:
268 | return LOCTEXT("Unknown_Tooltip", "Unknown source control state");
269 | case EWorkingCopyState::Unchanged:
270 | return LOCTEXT("Pristine_Tooltip", "There are no modifications");
271 | case EWorkingCopyState::Added:
272 | return LOCTEXT("Added_Tooltip", "Item is scheduled for addition");
273 | case EWorkingCopyState::Deleted:
274 | return LOCTEXT("Deleted_Tooltip", "Item is scheduled for deletion");
275 | case EWorkingCopyState::Modified:
276 | return LOCTEXT("Modified_Tooltip", "Item has been modified");
277 | case EWorkingCopyState::Renamed:
278 | return LOCTEXT("Renamed_Tooltip", "Item has been renamed");
279 | case EWorkingCopyState::Copied:
280 | return LOCTEXT("Copied_Tooltip", "Item has been copied");
281 | case EWorkingCopyState::Conflicted:
282 | return LOCTEXT("ContentsConflict_Tooltip", "The contents of the item conflict with updates received from the repository.");
283 | case EWorkingCopyState::Ignored:
284 | return LOCTEXT("Ignored_Tooltip", "Item is being ignored.");
285 | case EWorkingCopyState::NotControlled:
286 | return LOCTEXT("NotControlled_Tooltip", "Item is not under version control.");
287 | case EWorkingCopyState::Missing:
288 | return LOCTEXT("Missing_Tooltip", "Item is missing (e.g., you moved or deleted it without using Git). This also indicates that a directory is incomplete (a checkout or update was interrupted).");
289 | }
290 |
291 | return FText();
292 | }
293 |
294 | const FString& FGitSourceControlState::GetFilename() const
295 | {
296 | return LocalFilename;
297 | }
298 |
299 | const FDateTime& FGitSourceControlState::GetTimeStamp() const
300 | {
301 | return TimeStamp;
302 | }
303 |
304 | // Deleted and Missing assets cannot appear in the Content Browser, but the do in the Submit files to Source Control window!
305 | bool FGitSourceControlState::CanCheckIn() const
306 | {
307 | if (bUsingGitLfsLocking)
308 | {
309 | return (((LockState == ELockState::Locked) && !IsConflicted()) || (WorkingCopyState == EWorkingCopyState::Added)) && IsCurrent();
310 | }
311 | else
312 | {
313 | return (WorkingCopyState == EWorkingCopyState::Added
314 | || WorkingCopyState == EWorkingCopyState::Deleted
315 | || WorkingCopyState == EWorkingCopyState::Missing
316 | || WorkingCopyState == EWorkingCopyState::Modified
317 | || WorkingCopyState == EWorkingCopyState::Renamed) && IsCurrent();
318 | }
319 | }
320 |
321 | bool FGitSourceControlState::CanCheckout() const
322 | {
323 | if (bUsingGitLfsLocking)
324 | {
325 | // 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
326 | return (WorkingCopyState == EWorkingCopyState::Unchanged || WorkingCopyState == EWorkingCopyState::Modified) && LockState == ELockState::NotLocked && IsCurrent();
327 | }
328 | else
329 | {
330 | return false; // With Git all tracked files in the working copy are always already checked-out (as opposed to Perforce)
331 | }
332 | }
333 |
334 | bool FGitSourceControlState::IsCheckedOut() const
335 | {
336 | if (bUsingGitLfsLocking)
337 | {
338 | return LockState == ELockState::Locked;
339 | }
340 | else
341 | {
342 | return IsSourceControlled(); // With Git all tracked files in the working copy are always checked-out (as opposed to Perforce)
343 | }
344 | }
345 |
346 | bool FGitSourceControlState::IsCheckedOutOther(FString* Who) const
347 | {
348 | if (Who != NULL)
349 | {
350 | *Who = LockUser;
351 | }
352 | return LockState == ELockState::LockedOther;
353 | }
354 |
355 | bool FGitSourceControlState::IsCurrent() const
356 | {
357 | return !bNewerVersionOnServer;
358 | }
359 |
360 | bool FGitSourceControlState::IsSourceControlled() const
361 | {
362 | return WorkingCopyState != EWorkingCopyState::NotControlled && WorkingCopyState != EWorkingCopyState::Ignored && WorkingCopyState != EWorkingCopyState::Unknown;
363 | }
364 |
365 | bool FGitSourceControlState::IsAdded() const
366 | {
367 | return WorkingCopyState == EWorkingCopyState::Added;
368 | }
369 |
370 | bool FGitSourceControlState::IsDeleted() const
371 | {
372 | return WorkingCopyState == EWorkingCopyState::Deleted || WorkingCopyState == EWorkingCopyState::Missing;
373 | }
374 |
375 | bool FGitSourceControlState::IsIgnored() const
376 | {
377 | return WorkingCopyState == EWorkingCopyState::Ignored;
378 | }
379 |
380 | bool FGitSourceControlState::CanEdit() const
381 | {
382 | return IsCurrent(); // With Git all files in the working copy are always editable (as opposed to Perforce)
383 | }
384 |
385 | bool FGitSourceControlState::CanDelete() const
386 | {
387 | return !IsCheckedOutOther() && IsSourceControlled() && IsCurrent();
388 | }
389 |
390 | bool FGitSourceControlState::IsUnknown() const
391 | {
392 | return WorkingCopyState == EWorkingCopyState::Unknown;
393 | }
394 |
395 | bool FGitSourceControlState::IsModified() const
396 | {
397 | // Warning: for Perforce, a checked-out file is locked for modification (whereas with Git all tracked files are checked-out),
398 | // so for a clean "check-in" (commit) checked-out files unmodified should be removed from the changeset (the index)
399 | // http://stackoverflow.com/questions/12357971/what-does-revert-unchanged-files-mean-in-perforce
400 | //
401 | // Thus, before check-in UE Editor call RevertUnchangedFiles() in PromptForCheckin() and CheckinFiles().
402 | //
403 | // So here we must take care to enumerate all states that need to be commited,
404 | // all other will be discarded :
405 | // - Unknown
406 | // - Unchanged
407 | // - NotControlled
408 | // - Ignored
409 | return WorkingCopyState == EWorkingCopyState::Added
410 | || WorkingCopyState == EWorkingCopyState::Deleted
411 | || WorkingCopyState == EWorkingCopyState::Modified
412 | || WorkingCopyState == EWorkingCopyState::Renamed
413 | || WorkingCopyState == EWorkingCopyState::Copied
414 | || WorkingCopyState == EWorkingCopyState::Missing
415 | || WorkingCopyState == EWorkingCopyState::Conflicted;
416 | }
417 |
418 |
419 | bool FGitSourceControlState::CanAdd() const
420 | {
421 | return WorkingCopyState == EWorkingCopyState::NotControlled;
422 | }
423 |
424 | bool FGitSourceControlState::IsConflicted() const
425 | {
426 | return WorkingCopyState == EWorkingCopyState::Conflicted;
427 | }
428 |
429 | bool FGitSourceControlState::CanRevert() const
430 | {
431 | return CanCheckIn();
432 | }
433 |
434 | TSharedPtr FGitSourceControlState::GetCurrentRevision() const
435 | {
436 | return nullptr;
437 | }
438 |
439 | #undef LOCTEXT_NAMESPACE
440 |
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlState.h:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 | //
3 | // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
4 | // or copy at http://opensource.org/licenses/MIT)
5 |
6 | #pragma once
7 |
8 | #include "CoreMinimal.h"
9 | #include "ISourceControlState.h"
10 | #include "ISourceControlRevision.h"
11 | #include "GitSourceControlRevision.h"
12 |
13 | #include "Runtime/Launch/Resources/Version.h"
14 |
15 | namespace EWorkingCopyState
16 | {
17 | enum Type
18 | {
19 | Unknown,
20 | Unchanged,
21 | // called "clean" in SVN, "Pristine" in Perforce
22 | Added,
23 | Deleted,
24 | Modified,
25 | Renamed,
26 | Copied,
27 | Missing,
28 | Conflicted,
29 | NotControlled,
30 | Ignored,
31 | };
32 | }
33 |
34 | namespace ELockState
35 | {
36 | enum Type
37 | {
38 | Unknown,
39 | NotLocked,
40 | Locked,
41 | LockedOther,
42 | };
43 | }
44 |
45 | class FGitSourceControlState : public ISourceControlState
46 | {
47 | public:
48 | FGitSourceControlState(const FString& InLocalFilename, const bool InUsingLfsLocking)
49 | : LocalFilename(InLocalFilename)
50 | , WorkingCopyState(EWorkingCopyState::Unknown)
51 | , LockState(ELockState::Unknown)
52 | , bUsingGitLfsLocking(InUsingLfsLocking)
53 | , bNewerVersionOnServer(false)
54 | , TimeStamp(0)
55 | {
56 | }
57 |
58 | /** ISourceControlState interface */
59 | virtual int32 GetHistorySize() const override;
60 | virtual TSharedPtr GetHistoryItem(int32 HistoryIndex) const override;
61 | virtual TSharedPtr FindHistoryRevision(int32 RevisionNumber) const override;
62 | virtual TSharedPtr FindHistoryRevision(const FString& InRevision) const override;
63 | #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
64 | virtual FResolveInfo GetResolveInfo() const override;
65 | #else
66 | virtual TSharedPtr GetBaseRevForMerge() const override;
67 | #endif
68 | virtual TSharedPtr GetCurrentRevision() const; /* override NOTE: added in UE5.2 */
69 | #if ENGINE_MAJOR_VERSION == 5
70 | virtual FSlateIcon GetIcon() const override;
71 | #else
72 | virtual FName GetIconName() const override;
73 | virtual FName GetSmallIconName() const override;
74 | #endif
75 | virtual FText GetDisplayName() const override;
76 | virtual FText GetDisplayTooltip() const override;
77 | virtual const FString& GetFilename() const override;
78 | virtual const FDateTime& GetTimeStamp() const override;
79 | virtual bool CanCheckIn() const override;
80 | virtual bool CanCheckout() const override;
81 | virtual bool IsCheckedOut() const override;
82 | virtual bool IsCheckedOutOther(FString* Who = nullptr) const override;
83 | virtual bool IsCheckedOutInOtherBranch(const FString& CurrentBranch = FString()) const override { return false; }
84 | virtual bool IsModifiedInOtherBranch(const FString& CurrentBranch = FString()) const override { return false; }
85 | virtual bool IsCheckedOutOrModifiedInOtherBranch(const FString& CurrentBranch = FString()) const override { return IsCheckedOutInOtherBranch(CurrentBranch) || IsModifiedInOtherBranch(CurrentBranch); }
86 | virtual TArray GetCheckedOutBranches() const override { return TArray(); }
87 | virtual FString GetOtherUserBranchCheckedOuts() const override { return FString(); }
88 | virtual bool GetOtherBranchHeadModification(FString& HeadBranchOut, FString& ActionOut, int32& HeadChangeListOut) const override { return false; }
89 | virtual bool IsCurrent() const override;
90 | virtual bool IsSourceControlled() const override;
91 | virtual bool IsAdded() const override;
92 | virtual bool IsDeleted() const override;
93 | virtual bool IsIgnored() const override;
94 | virtual bool CanEdit() const override;
95 | virtual bool IsUnknown() const override;
96 | virtual bool IsModified() const override;
97 | virtual bool CanAdd() const override;
98 | virtual bool CanDelete() const override;
99 | virtual bool IsConflicted() const override;
100 | virtual bool CanRevert() const override;
101 |
102 | public:
103 | /** History of the item, if any */
104 | TGitSourceControlHistory History;
105 |
106 | /** Filename on disk */
107 | FString LocalFilename;
108 |
109 | #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
110 | /** Pending rev info with which a file must be resolved, invalid if no resolve pending */
111 | FResolveInfo PendingResolveInfo;
112 | #else
113 | /** File Id with which our local revision diverged from the remote revision */
114 | FString PendingMergeBaseFileHash;
115 | #endif
116 |
117 | /** State of the working copy */
118 | EWorkingCopyState::Type WorkingCopyState;
119 |
120 | /** Lock state */
121 | ELockState::Type LockState;
122 |
123 | /** Name of user who has locked the file */
124 | FString LockUser;
125 |
126 | /** Tells if using the Git LFS file Locking workflow */
127 | bool bUsingGitLfsLocking;
128 |
129 | /** Whether a newer version exists on the server */
130 | bool bNewerVersionOnServer;
131 |
132 | /** The timestamp of the last update */
133 | FDateTime TimeStamp;
134 | };
135 |
--------------------------------------------------------------------------------
/Source/GitSourceControl/Private/GitSourceControlUtils.h:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2022 Sebastien Rombauts (sebastien.rombauts@gmail.com)
2 | //
3 | // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
4 | // or copy at http://opensource.org/licenses/MIT)
5 |
6 | #pragma once
7 |
8 | #include "CoreMinimal.h"
9 | #include "GitSourceControlState.h"
10 |
11 | class FGitSourceControlCommand;
12 |
13 | /**
14 | * Helper struct for maintaining temporary files for passing to commands
15 | */
16 | class FGitScopedTempFile
17 | {
18 | public:
19 |
20 | /** Constructor - open & write string to temp file */
21 | FGitScopedTempFile(const FText& InText);
22 |
23 | /** Destructor - delete temp file */
24 | ~FGitScopedTempFile();
25 |
26 | /** Get the filename of this temp file - empty if it failed to be created */
27 | const FString& GetFilename() const;
28 |
29 | private:
30 | /** The filename we are writing to */
31 | FString Filename;
32 | };
33 |
34 | struct FGitVersion;
35 |
36 | namespace GitSourceControlUtils
37 | {
38 |
39 | /**
40 | * Find the path to the Git binary, looking into a few places (standalone Git install, and other common tools embedding Git)
41 | * @returns the path to the Git binary if found, or an empty string.
42 | */
43 | FString FindGitBinaryPath();
44 |
45 | /**
46 | * Run a Git "version" command to check the availability of the binary.
47 | * @param InPathToGitBinary The path to the Git binary
48 | * @param OutGitVersion If provided, populate with the git version parsed from "version" command
49 | * @returns true if the command succeeded and returned no errors
50 | */
51 | bool CheckGitAvailability(const FString& InPathToGitBinary, FGitVersion* OutVersion = nullptr);
52 |
53 | /**
54 | * Parse the output from the "version" command into GitMajorVersion and GitMinorVersion.
55 | * @param InVersionString The version string returned by `git --version`
56 | * @param OutVersion The FGitVersion to populate
57 | */
58 | void ParseGitVersion(const FString& InVersionString, FGitVersion* OutVersion);
59 |
60 | /**
61 | * Check git for various optional capabilities by various means.
62 | * @param InPathToGitBinary The path to the Git binary
63 | * @param OutGitVersion If provided, populate with the git version parsed from "version" command
64 | */
65 | void FindGitCapabilities(const FString& InPathToGitBinary, FGitVersion *OutVersion);
66 |
67 | /**
68 | * Run a Git "lfs" command to check the availability of the "Large File System" extension.
69 | * @param InPathToGitBinary The path to the Git binary
70 | * @param OutGitVersion If provided, populate with the git version parsed from "version" command
71 | */
72 | void FindGitLfsCapabilities(const FString& InPathToGitBinary, FGitVersion *OutVersion);
73 |
74 | /**
75 | * Find the root of the Git repository, looking from the provided path and upward in its parent directories
76 | * @param InPath The path to the Game Directory (or any path or file in any git repository)
77 | * @param OutRepositoryRoot The path to the root directory of the Git repository if found, else the path to the ProjectDir
78 | * @returns true if the command succeeded and returned no errors
79 | */
80 | bool FindRootDirectory(const FString& InPath, FString& OutRepositoryRoot);
81 |
82 | /**
83 | * Get Git config user.name & user.email
84 | * @param InPathToGitBinary The path to the Git binary
85 | * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory (can be empty)
86 | * @param OutUserName Name of the Git user configured for this repository (or globaly)
87 | * @param OutEmailName E-mail of the Git user configured for this repository (or globaly)
88 | */
89 | void GetUserConfig(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutUserName, FString& OutUserEmail);
90 |
91 | /**
92 | * Get Git current checked-out branch
93 | * @param InPathToGitBinary The path to the Git binary
94 | * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory
95 | * @param OutBranchName Name of the current checked-out branch (if any, ie. not in detached HEAD)
96 | * @returns true if the command succeeded and returned no errors
97 | */
98 | bool GetBranchName(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutBranchName);
99 |
100 | /**
101 | * Get Git current commit details
102 | * @param InPathToGitBinary The path to the Git binary
103 | * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory
104 | * @param OutCommitId Current Commit full SHA1
105 | * @param OutCommitSummary Current Commit description's Summary
106 | * @returns true if the command succeeded and returned no errors
107 | */
108 | bool GetCommitInfo(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutCommitId, FString& OutCommitSummary);
109 |
110 | /**
111 | * Get the URL of the "origin" defaut remote server
112 | * @param InPathToGitBinary The path to the Git binary
113 | * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory
114 | * @param OutRemoteUrl URL of "origin" defaut remote server
115 | * @returns true if the command succeeded and returned no errors
116 | */
117 | bool GetRemoteUrl(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutRemoteUrl);
118 |
119 | /**
120 | * Run a Git command - output is a string TArray.
121 | *
122 | * @param InCommand The Git command - e.g. commit
123 | * @param InPathToGitBinary The path to the Git binary
124 | * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory (can be empty)
125 | * @param InParameters The parameters to the Git command
126 | * @param InFiles The files to be operated on
127 | * @param OutResults The results (from StdOut) as an array per-line
128 | * @param OutErrorMessages Any errors (from StdErr) as an array per-line
129 | * @returns true if the command succeeded and returned no errors
130 | */
131 | bool RunCommand(const FString& InCommand, const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages);
132 | bool RunCommandInternalRaw(const FString& InCommand, const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InParameters, const TArray& InFiles, FString& OutResults, FString& OutErrors, const int32 ExpectedReturnCode = 0);
133 |
134 | /**
135 | * Run a Git "commit" command by batches.
136 | *
137 | * @param InPathToGitBinary The path to the Git binary
138 | * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory
139 | * @param InParameter The parameters to the Git commit command
140 | * @param InFiles The files to be operated on
141 | * @param OutErrorMessages Any errors (from StdErr) as an array per-line
142 | * @returns true if the command succeeded and returned no errors
143 | */
144 | bool RunCommit(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray