├── .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 | [![release](https://img.shields.io/github/release/SRombauts/UE4GitPlugin.svg)](https://github.com/SRombauts/UE4GitPlugin/releases) 5 | [![Git Plugin issues](https://img.shields.io/github/issues/SRombauts/UE4GitPlugin.svg)](https://github.com/SRombauts/UE4GitPlugin/issues) 6 | [![Join the chat at https://gitter.im/SRombauts/UE4GitPlugin](https://badges.gitter.im/Join%20Chat.svg)](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 | ![Source Control Login window - create a new repository](Screenshots/SourceControlLogin_Init.png) 26 | 27 | Source Control status tooltip, when hovering the Source Control icon in toolbar: 28 | ![Source Control Status Tooltip](Screenshots/SourceControlStatusTooltip.png) 29 | 30 | Source Control top Menu, extended with a few commands specific to Git: 31 | ![Source Control Status Tooltip](Screenshots/SourceControlMenu.png) 32 | 33 | Submit Files to Source Control window, to commit assets: 34 | ![Submit Files to Source Control](Screenshots/SubmitFiles.png) 35 | 36 | File History window, to see the changelog of an asset: 37 | ![History of a file](Screenshots/FileHistory.png) 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 | ![New/Unsaved/Untracked](Screenshots/Icons/New.png) 48 | ![Added](Screenshots/Icons/Added.png) 49 | ![Unchanged](Screenshots/Icons/Unchanged.png) 50 | ![Modified](Screenshots/Icons/Modified.png) 51 | ![Moved/Renamed](Screenshots/Icons/Renamed.png) 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 | [![Git Plugin on Unreal Engine 4.12 (preview)](https://img.youtube.com/vi/rRhPl9vL58Q/0.jpg)](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& OutErrorMessages); 145 | 146 | /** 147 | * Run a Git "status" command and parse it. 148 | * 149 | * @param InPathToGitBinary The path to the Git binary 150 | * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory (can be empty) 151 | * @param InUsingLfsLocking Tells if using the Git LFS file Locking workflow 152 | * @param InFiles The files to be operated on 153 | * @param OutErrorMessages Any errors (from StdErr) as an array per-line 154 | * @returns true if the command succeeded and returned no errors 155 | */ 156 | bool RunUpdateStatus(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool InUsingLfsLocking, const TArray& InFiles, TArray& OutErrorMessages, TArray& OutStates); 157 | 158 | /** 159 | * Run a Git "cat-file" command to dump the binary content of a revision into a file. 160 | * 161 | * @param InPathToGitBinary The path to the Git binary 162 | * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory 163 | * @param InParameter The parameters to the Git show command (rev:path) 164 | * @param InDumpFileName The temporary file to dump the revision 165 | * @returns true if the command succeeded and returned no errors 166 | */ 167 | bool RunDumpToFile(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InParameter, const FString& InDumpFileName); 168 | 169 | /** 170 | * Run a Git "log" command and parse it. 171 | * 172 | * @param InPathToGitBinary The path to the Git binary 173 | * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory 174 | * @param InFile The file to be operated on 175 | * @param bMergeConflict In case of a merge conflict, we also need to get the tip of the "remote branch" (MERGE_HEAD) before the log of the "current branch" (HEAD) 176 | * @param OutErrorMessages Any errors (from StdErr) as an array per-line 177 | * @param OutHistory The history of the file 178 | */ 179 | bool RunGetHistory(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InFile, bool bMergeConflict, TArray& OutErrorMessages, TGitSourceControlHistory& OutHistory); 180 | 181 | /** 182 | * Helper function to convert a filename array to relative paths. 183 | * @param InFileNames The filename array 184 | * @param InRelativeTo Path to the WorkspaceRoot 185 | * @return an array of filenames, transformed into relative paths 186 | */ 187 | TArray RelativeFilenames(const TArray& InFileNames, const FString& InRelativeTo); 188 | 189 | /** 190 | * Helper function to convert a filename array to absolute paths. 191 | * @param InFileNames The filename array (relative paths) 192 | * @param InRelativeTo Path to the WorkspaceRoot 193 | * @return an array of filenames, transformed into absolute paths 194 | */ 195 | TArray AbsoluteFilenames(const TArray& InFileNames, const FString& InRelativeTo); 196 | 197 | /** 198 | * Helper function for various commands to update cached states. 199 | * @returns true if any states were updated 200 | */ 201 | bool UpdateCachedStates(const TArray& InStates); 202 | 203 | /** 204 | * Remove redundant errors (that contain a particular string) and also 205 | * update the commands success status if all errors were removed. 206 | */ 207 | void RemoveRedundantErrors(FGitSourceControlCommand& InCommand, const FString& InFilter); 208 | 209 | /** 210 | * Run 'git lfs locks" to extract all lock information for all files in the repository 211 | * 212 | * @param InPathToGitBinary The path to the Git binary 213 | * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory 214 | * @param bAbsolutePaths Whether to report absolute filenames, false for repo-relative 215 | * @param OutErrorMessages Any errors (from StdErr) as an array per-line 216 | * @param OutLocks The lock results (file, username) 217 | * @returns true if the command succeeded and returned no errors 218 | */ 219 | bool GetAllLocks(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool bAbsolutePaths, TArray& OutErrorMessages, TMap& OutLocks); 220 | 221 | } 222 | -------------------------------------------------------------------------------- /Source/GitSourceControl/Private/IGitSourceControlWorker.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 IGitSourceControlWorker 11 | { 12 | public: 13 | /** 14 | * Name describing the work that this worker does. Used for factory method hookup. 15 | */ 16 | virtual FName GetName() const = 0; 17 | 18 | /** 19 | * Function that actually does the work. Can be executed on another thread. 20 | */ 21 | virtual bool Execute( class FGitSourceControlCommand& InCommand ) = 0; 22 | 23 | /** 24 | * Updates the state of any items after completion (if necessary). This is always executed on the main thread. 25 | * @returns true if states were updated 26 | */ 27 | virtual bool UpdateStates() const = 0; 28 | }; 29 | 30 | typedef TSharedRef FGitSourceControlWorkerRef; 31 | -------------------------------------------------------------------------------- /Source/GitSourceControl/Private/SGitSourceControlSettings.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 "SGitSourceControlSettings.h" 7 | 8 | #include "Fonts/SlateFontInfo.h" 9 | #include "Misc/App.h" 10 | #include "Misc/FileHelper.h" 11 | #include "Misc/Paths.h" 12 | #include "Modules/ModuleManager.h" 13 | #include "Styling/SlateTypes.h" 14 | #include "Widgets/SBoxPanel.h" 15 | #include "Widgets/Text/STextBlock.h" 16 | #include "Widgets/Input/SButton.h" 17 | #include "Widgets/Input/SCheckBox.h" 18 | #include "Widgets/Input/SEditableTextBox.h" 19 | #include "Widgets/Input/SFilePathPicker.h" 20 | #include "Widgets/Input/SMultiLineEditableTextBox.h" 21 | #include "Widgets/Layout/SBorder.h" 22 | #include "Widgets/Layout/SSeparator.h" 23 | #include "Widgets/Notifications/SNotificationList.h" 24 | #include "Framework/Notifications/NotificationManager.h" 25 | #include "EditorDirectories.h" 26 | #include "Runtime/Launch/Resources/Version.h" 27 | #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 28 | #include "Styling/AppStyle.h" 29 | #else 30 | #include "EditorStyleSet.h" 31 | #endif 32 | #include "SourceControlOperations.h" 33 | #include "GitSourceControlModule.h" 34 | #include "GitSourceControlUtils.h" 35 | 36 | #define LOCTEXT_NAMESPACE "SGitSourceControlSettings" 37 | 38 | void SGitSourceControlSettings::Construct(const FArguments& InArgs) 39 | { 40 | #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 41 | const FSlateFontInfo Font = FAppStyle::GetFontStyle(TEXT("SourceControl.LoginWindow.Font")); 42 | #else 43 | const FSlateFontInfo Font = FEditorStyle::GetFontStyle(TEXT("SourceControl.LoginWindow.Font")); 44 | #endif 45 | 46 | bAutoCreateGitIgnore = true; 47 | bAutoCreateReadme = true; 48 | bAutoCreateGitAttributes = false; 49 | bAutoInitialCommit = true; 50 | 51 | InitialCommitMessage = LOCTEXT("InitialCommitMessage", "Initial commit"); 52 | 53 | const FText FileFilterType = NSLOCTEXT("GitSourceControl", "Executables", "Executables"); 54 | #if PLATFORM_WINDOWS 55 | const FString FileFilterText = FString::Printf(TEXT("%s (*.exe)|*.exe"), *FileFilterType.ToString()); 56 | #else 57 | const FString FileFilterText = FString::Printf(TEXT("%s"), *FileFilterType.ToString()); 58 | #endif 59 | 60 | ReadmeContent = FText::FromString(FString(TEXT("# ")) + FApp::GetProjectName() + "\n\nDeveloped with Unreal Engine 4\n"); 61 | 62 | ChildSlot 63 | [ 64 | #if ENGINE_MAJOR_VERSION == 4 65 | SNew(SBorder) 66 | .BorderImage(FEditorStyle::GetBrush("DetailsView.CategoryBottom")) 67 | .Padding(FMargin(0.0f, 3.0f, 0.0f, 0.0f)) 68 | [ 69 | #endif 70 | SNew(SVerticalBox) 71 | // Path to the Git command line executable 72 | + SVerticalBox::Slot() 73 | .AutoHeight() 74 | .Padding(2.0f) 75 | .VAlign(VAlign_Center) 76 | [ 77 | SNew(SHorizontalBox) 78 | .ToolTipText(LOCTEXT("BinaryPathLabel_Tooltip", "Path to Git binary")) 79 | + SHorizontalBox::Slot() 80 | .FillWidth(1.0f) 81 | [ 82 | SNew(STextBlock) 83 | .Text(LOCTEXT("BinaryPathLabel", "Git Path")) 84 | .Font(Font) 85 | ] 86 | + SHorizontalBox::Slot() 87 | .FillWidth(2.0f) 88 | [ 89 | SNew(SFilePathPicker) 90 | #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 91 | .BrowseButtonImage(FAppStyle::Get().GetBrush("PropertyWindow.Button_Ellipsis")) 92 | .BrowseButtonStyle(FAppStyle::Get(), "HoverHintOnly") 93 | #else 94 | .BrowseButtonImage(FEditorStyle::GetBrush("PropertyWindow.Button_Ellipsis")) 95 | .BrowseButtonStyle(FEditorStyle::Get(), "HoverHintOnly") 96 | #endif 97 | .BrowseDirectory(FEditorDirectories::Get().GetLastDirectory(ELastDirectory::GENERIC_OPEN)) 98 | .BrowseTitle(LOCTEXT("BinaryPathBrowseTitle", "File picker...")) 99 | .FilePath(this, &SGitSourceControlSettings::GetBinaryPathString) 100 | .FileTypeFilter(FileFilterText) 101 | .OnPathPicked(this, &SGitSourceControlSettings::OnBinaryPathPicked) 102 | ] 103 | ] 104 | // Root of the local repository 105 | + SVerticalBox::Slot() 106 | .AutoHeight() 107 | .Padding(2.0f) 108 | .VAlign(VAlign_Center) 109 | [ 110 | SNew(SHorizontalBox) 111 | .ToolTipText(LOCTEXT("RepositoryRootLabel_Tooltip", "Path to the root of the Git repository")) 112 | + SHorizontalBox::Slot() 113 | .FillWidth(1.0f) 114 | [ 115 | SNew(STextBlock) 116 | .Text(LOCTEXT("RepositoryRootLabel", "Root of the repository")) 117 | .Font(Font) 118 | ] 119 | + SHorizontalBox::Slot() 120 | .FillWidth(2.0f) 121 | [ 122 | SNew(STextBlock) 123 | .Text(this, &SGitSourceControlSettings::GetPathToRepositoryRoot) 124 | .Font(Font) 125 | ] 126 | ] 127 | // User Name 128 | + SVerticalBox::Slot() 129 | .AutoHeight() 130 | .Padding(2.0f) 131 | .VAlign(VAlign_Center) 132 | [ 133 | SNew(SHorizontalBox) 134 | .ToolTipText(LOCTEXT("GitUserName_Tooltip", "User name configured for the Git repository")) 135 | + SHorizontalBox::Slot() 136 | .FillWidth(1.0f) 137 | [ 138 | SNew(STextBlock) 139 | .Text(LOCTEXT("GitUserName", "User Name")) 140 | .Font(Font) 141 | ] 142 | + SHorizontalBox::Slot() 143 | .FillWidth(2.0f) 144 | [ 145 | SNew(STextBlock) 146 | .Text(this, &SGitSourceControlSettings::GetUserName) 147 | .Font(Font) 148 | ] 149 | ] 150 | // User e-mail 151 | + SVerticalBox::Slot() 152 | .FillHeight(1.0f) 153 | .Padding(2.0f) 154 | .VAlign(VAlign_Center) 155 | [ 156 | SNew(SHorizontalBox) 157 | .ToolTipText(LOCTEXT("GitUserEmail_Tooltip", "User e-mail configured for the Git repository")) 158 | + SHorizontalBox::Slot() 159 | .FillWidth(1.0f) 160 | [ 161 | SNew(STextBlock) 162 | .Text(LOCTEXT("GitUserEmail", "E-Mail")) 163 | .Font(Font) 164 | ] 165 | + SHorizontalBox::Slot() 166 | .FillWidth(2.0f) 167 | [ 168 | SNew(STextBlock) 169 | .Text(this, &SGitSourceControlSettings::GetUserEmail) 170 | .Font(Font) 171 | ] 172 | ] 173 | // Separator 174 | + SVerticalBox::Slot() 175 | .AutoHeight() 176 | .Padding(2.0f) 177 | .VAlign(VAlign_Center) 178 | [ 179 | SNew(SSeparator) 180 | ] 181 | // Explanation text 182 | + SVerticalBox::Slot() 183 | .FillHeight(1.0f) 184 | .Padding(2.0f) 185 | .VAlign(VAlign_Center) 186 | [ 187 | SNew(SHorizontalBox) 188 | .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) 189 | + SHorizontalBox::Slot() 190 | .FillWidth(1.0f) 191 | .HAlign(HAlign_Center) 192 | [ 193 | SNew(STextBlock) 194 | .Text(LOCTEXT("RepositoryNotFound", "Current Project is not contained in a Git Repository. Fill the form below to initialize a new Repository.")) 195 | .ToolTipText(LOCTEXT("RepositoryNotFound_Tooltip", "No Repository found at the level or above the current Project")) 196 | .Font(Font) 197 | ] 198 | ] 199 | // Option to configure the URL of the default remote 'origin' 200 | // TODO: option to configure the name of the remote instead of the default origin 201 | + SVerticalBox::Slot() 202 | .AutoHeight() 203 | .Padding(2.0f) 204 | .VAlign(VAlign_Center) 205 | [ 206 | SNew(SHorizontalBox) 207 | .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) 208 | .ToolTipText(LOCTEXT("ConfigureOrigin_Tooltip", "Configure the URL of the default remote 'origin'")) 209 | + SHorizontalBox::Slot() 210 | .FillWidth(1.0f) 211 | [ 212 | SNew(STextBlock) 213 | .Text(LOCTEXT("ConfigureOrigin", "URL of the remote server 'origin'")) 214 | .Font(Font) 215 | ] 216 | + SHorizontalBox::Slot() 217 | .FillWidth(2.0f) 218 | .VAlign(VAlign_Center) 219 | [ 220 | SNew(SEditableTextBox) 221 | .Text(this, &SGitSourceControlSettings::GetRemoteUrl) 222 | .OnTextCommitted(this, &SGitSourceControlSettings::OnRemoteUrlCommited) 223 | .Font(Font) 224 | ] 225 | ] 226 | // Option to add a proper .gitignore file (true by default) 227 | + SVerticalBox::Slot() 228 | .AutoHeight() 229 | .Padding(2.0f) 230 | .VAlign(VAlign_Center) 231 | [ 232 | SNew(SHorizontalBox) 233 | .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) 234 | .ToolTipText(LOCTEXT("CreateGitIgnore_Tooltip", "Create and add a standard '.gitignore' file")) 235 | + SHorizontalBox::Slot() 236 | .FillWidth(0.1f) 237 | [ 238 | SNew(SCheckBox) 239 | .IsChecked(ECheckBoxState::Checked) 240 | .OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedCreateGitIgnore) 241 | ] 242 | + SHorizontalBox::Slot() 243 | .FillWidth(2.9f) 244 | .VAlign(VAlign_Center) 245 | [ 246 | SNew(STextBlock) 247 | .Text(LOCTEXT("CreateGitIgnore", "Add a .gitignore file")) 248 | .Font(Font) 249 | ] 250 | ] 251 | // Option to add a README.md file with custom content 252 | + SVerticalBox::Slot() 253 | .AutoHeight() 254 | .Padding(2.0f) 255 | .VAlign(VAlign_Center) 256 | [ 257 | SNew(SHorizontalBox) 258 | .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) 259 | .ToolTipText(LOCTEXT("CreateReadme_Tooltip", "Add a README.md file")) 260 | + SHorizontalBox::Slot() 261 | .FillWidth(0.1f) 262 | [ 263 | SNew(SCheckBox) 264 | .IsChecked(ECheckBoxState::Checked) 265 | .OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedCreateReadme) 266 | ] 267 | + SHorizontalBox::Slot() 268 | .FillWidth(0.9f) 269 | .VAlign(VAlign_Center) 270 | [ 271 | SNew(STextBlock) 272 | .Text(LOCTEXT("CreateReadme", "Add a basic README.md file")) 273 | .Font(Font) 274 | ] 275 | + SHorizontalBox::Slot() 276 | .FillWidth(2.0f) 277 | .Padding(2.0f) 278 | [ 279 | SNew(SMultiLineEditableTextBox) 280 | .Text(this, &SGitSourceControlSettings::GetReadmeContent) 281 | .OnTextCommitted(this, &SGitSourceControlSettings::OnReadmeContentCommited) 282 | .IsEnabled(this, &SGitSourceControlSettings::GetAutoCreateReadme) 283 | .SelectAllTextWhenFocused(true) 284 | .Font(Font) 285 | ] 286 | ] 287 | // Option to add a proper .gitattributes file for Git LFS (false by default) 288 | + SVerticalBox::Slot() 289 | .AutoHeight() 290 | .Padding(2.0f) 291 | .VAlign(VAlign_Center) 292 | [ 293 | SNew(SHorizontalBox) 294 | .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) 295 | .ToolTipText(LOCTEXT("CreateGitAttributes_Tooltip", "Create and add a '.gitattributes' file to enable Git LFS for the whole 'Content/' directory (needs Git LFS extensions to be installed).")) 296 | + SHorizontalBox::Slot() 297 | .FillWidth(0.1f) 298 | [ 299 | SNew(SCheckBox) 300 | .IsChecked(ECheckBoxState::Unchecked) 301 | .OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedCreateGitAttributes) 302 | .IsEnabled(this, &SGitSourceControlSettings::CanInitializeGitLfs) 303 | ] 304 | + SHorizontalBox::Slot() 305 | .FillWidth(2.9f) 306 | .VAlign(VAlign_Center) 307 | [ 308 | SNew(STextBlock) 309 | .Text(LOCTEXT("CreateGitAttributes", "Add a .gitattributes file to enable Git LFS")) 310 | .Font(Font) 311 | ] 312 | ] 313 | // Option to use the Git LFS File Locking workflow (false by default) 314 | // Enabled even after init to switch it off in case of no network 315 | // TODO LFS turning it off afterwards does not work because all files are readonly ! 316 | + SVerticalBox::Slot() 317 | .AutoHeight() 318 | .Padding(2.0f) 319 | .VAlign(VAlign_Center) 320 | [ 321 | SNew(SHorizontalBox) 322 | .ToolTipText(LOCTEXT("UseGitLfsLocking_Tooltip", "Uses Git LFS 2 File Locking workflow (CheckOut and Commit/Push).")) 323 | + SHorizontalBox::Slot() 324 | .FillWidth(0.1f) 325 | [ 326 | SNew(SCheckBox) 327 | .IsChecked(SGitSourceControlSettings::IsUsingGitLfsLocking()) 328 | .OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedUseGitLfsLocking) 329 | .IsEnabled(this, &SGitSourceControlSettings::CanUseGitLfsLocking) 330 | ] 331 | + SHorizontalBox::Slot() 332 | .FillWidth(0.9f) 333 | .VAlign(VAlign_Center) 334 | [ 335 | SNew(STextBlock) 336 | .Text(LOCTEXT("UseGitLfsLocking", "Uses Git LFS 2 File Locking workflow")) 337 | .Font(Font) 338 | ] 339 | // Username credential used to access the Git LFS 2 File Locks server 340 | + SHorizontalBox::Slot() 341 | .FillWidth(2.0f) 342 | .VAlign(VAlign_Center) 343 | [ 344 | SNew(SEditableTextBox) 345 | .Text(this, &SGitSourceControlSettings::GetLfsUserName) 346 | .OnTextCommitted(this, &SGitSourceControlSettings::OnLfsUserNameCommited) 347 | .IsEnabled(this, &SGitSourceControlSettings::GetIsUsingGitLfsLocking) 348 | .HintText(LOCTEXT("LfsUserName_Hint", "Username to lock files on the LFS server")) 349 | .Font(Font) 350 | ] 351 | ] 352 | // Option that can be disabled to make Submit ONLY commit and not push 353 | // This is useful in these cases: 354 | // - To do a bunch of local commits more quickly and push in one go 355 | // - Working disconnected 356 | // - To combine asset changes with C++ changes in one commit; you can amend the UE commit to add the C++ changes before push 357 | // Push can be used separately and will unlock files if using LFS locking. 358 | + SVerticalBox::Slot() 359 | .AutoHeight() 360 | .Padding(2.0f) 361 | .VAlign(VAlign_Center) 362 | [ 363 | SNew(SHorizontalBox) 364 | .ToolTipText(LOCTEXT("GitPushAfterCommit_Tooltip", "Always try to Push (and unlock) after Commit on Submit when using LFS. Turning this off means you have to Push separately; Push will unlock LFS files.")) 365 | + SHorizontalBox::Slot() 366 | .FillWidth(0.1f) 367 | [ 368 | SNew(SCheckBox) 369 | .IsChecked(SGitSourceControlSettings::IsPushAfterCommitEnabled()) 370 | .OnCheckStateChanged(this, &SGitSourceControlSettings::OnIsPushAfterCommitEnabled) 371 | .IsEnabled(this, &SGitSourceControlSettings::GetIsUsingGitLfsLocking) 372 | ] 373 | + SHorizontalBox::Slot() 374 | .FillWidth(3.f) 375 | .VAlign(VAlign_Center) 376 | [ 377 | SNew(STextBlock) 378 | .Text(LOCTEXT("GitPushAfterCommit", "Submit means Commit AND Push")) 379 | .Font(Font) 380 | ] 381 | ] 382 | // Option to Make the initial Git commit with custom message 383 | + SVerticalBox::Slot() 384 | .AutoHeight() 385 | .Padding(2.0f) 386 | .VAlign(VAlign_Center) 387 | [ 388 | SNew(SHorizontalBox) 389 | .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) 390 | .ToolTipText(LOCTEXT("InitialGitCommit_Tooltip", "Make the initial Git commit")) 391 | + SHorizontalBox::Slot() 392 | .FillWidth(0.1f) 393 | [ 394 | SNew(SCheckBox) 395 | .IsChecked(ECheckBoxState::Checked) 396 | .OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedInitialCommit) 397 | ] 398 | + SHorizontalBox::Slot() 399 | .FillWidth(0.9f) 400 | .VAlign(VAlign_Center) 401 | [ 402 | SNew(STextBlock) 403 | .Text(LOCTEXT("InitialGitCommit", "Make the initial Git commit")) 404 | .Font(Font) 405 | ] 406 | + SHorizontalBox::Slot() 407 | .FillWidth(2.0f) 408 | .Padding(2.0f) 409 | [ 410 | SNew(SMultiLineEditableTextBox) 411 | .Text(this, &SGitSourceControlSettings::GetInitialCommitMessage) 412 | .OnTextCommitted(this, &SGitSourceControlSettings::OnInitialCommitMessageCommited) 413 | .IsEnabled(this, &SGitSourceControlSettings::GetAutoInitialCommit) 414 | .SelectAllTextWhenFocused(true) 415 | .Font(Font) 416 | ] 417 | ] 418 | // Button to initialize the project with Git, create .gitignore/.gitattributes files, and make the first commit) 419 | + SVerticalBox::Slot() 420 | .FillHeight(2.5f) 421 | .Padding(4.0f) 422 | .VAlign(VAlign_Center) 423 | [ 424 | SNew(SHorizontalBox) 425 | .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) 426 | + SHorizontalBox::Slot() 427 | .FillWidth(1.0f) 428 | [ 429 | SNew(SButton) 430 | .Text(LOCTEXT("GitInitRepository", "Initialize project with Git")) 431 | .ToolTipText(LOCTEXT("GitInitRepository_Tooltip", "Initialize current project as a new Git repository")) 432 | .OnClicked(this, &SGitSourceControlSettings::OnClickedInitializeGitRepository) 433 | .IsEnabled(this, &SGitSourceControlSettings::CanInitializeGitRepository) 434 | .HAlign(HAlign_Center) 435 | .ContentPadding(6) 436 | ] 437 | ] 438 | #if ENGINE_MAJOR_VERSION == 4 439 | ] 440 | #endif 441 | ]; 442 | } 443 | 444 | SGitSourceControlSettings::~SGitSourceControlSettings() 445 | { 446 | RemoveInProgressNotification(); 447 | } 448 | 449 | FString SGitSourceControlSettings::GetBinaryPathString() const 450 | { 451 | const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); 452 | return GitSourceControl.AccessSettings().GetBinaryPath(); 453 | } 454 | 455 | void SGitSourceControlSettings::OnBinaryPathPicked(const FString& PickedPath) const 456 | { 457 | FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); 458 | FString PickedFullPath = FPaths::ConvertRelativePathToFull(PickedPath); 459 | const bool bChanged = GitSourceControl.AccessSettings().SetBinaryPath(PickedFullPath); 460 | if (bChanged) 461 | { 462 | // Re-Check provided git binary path for each change 463 | GitSourceControl.GetProvider().CheckGitAvailability(); 464 | if (GitSourceControl.GetProvider().IsGitAvailable()) 465 | { 466 | GitSourceControl.SaveSettings(); 467 | } 468 | } 469 | } 470 | 471 | FText SGitSourceControlSettings::GetPathToRepositoryRoot() const 472 | { 473 | const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); 474 | return FText::FromString(GitSourceControl.GetProvider().GetPathToRepositoryRoot()); 475 | } 476 | 477 | FText SGitSourceControlSettings::GetUserName() const 478 | { 479 | const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); 480 | return FText::FromString(GitSourceControl.GetProvider().GetUserName()); 481 | } 482 | 483 | FText SGitSourceControlSettings::GetUserEmail() const 484 | { 485 | const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); 486 | return FText::FromString(GitSourceControl.GetProvider().GetUserEmail()); 487 | } 488 | 489 | EVisibility SGitSourceControlSettings::MustInitializeGitRepository() const 490 | { 491 | const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); 492 | const bool bGitAvailable = GitSourceControl.GetProvider().IsGitAvailable(); 493 | const bool bGitRepositoryFound = GitSourceControl.GetProvider().IsEnabled(); 494 | return (bGitAvailable && !bGitRepositoryFound) ? EVisibility::Visible : EVisibility::Collapsed; 495 | } 496 | 497 | bool SGitSourceControlSettings::CanInitializeGitRepository() const 498 | { 499 | const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); 500 | const bool bGitAvailable = GitSourceControl.GetProvider().IsGitAvailable(); 501 | const bool bGitRepositoryFound = GitSourceControl.GetProvider().IsEnabled(); 502 | const FString LfsUserName = GitSourceControl.AccessSettings().GetLfsUserName(); 503 | const bool bIsUsingGitLfsLocking = GitSourceControl.AccessSettings().IsUsingGitLfsLocking(); 504 | const bool bGitLfsConfigOk = !bIsUsingGitLfsLocking || !LfsUserName.IsEmpty(); 505 | const bool bInitialCommitConfigOk = !bAutoInitialCommit || !InitialCommitMessage.IsEmpty(); 506 | return (bGitAvailable && !bGitRepositoryFound && bGitLfsConfigOk && bInitialCommitConfigOk); 507 | } 508 | 509 | bool SGitSourceControlSettings::CanInitializeGitLfs() const 510 | { 511 | const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); 512 | const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath(); 513 | const bool bGitLfsAvailable = GitSourceControl.GetProvider().GetGitVersion().bHasGitLfs; 514 | return bGitLfsAvailable; 515 | } 516 | 517 | bool SGitSourceControlSettings::CanUseGitLfsLocking() const 518 | { 519 | const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); 520 | const bool bGitLfsLockingAvailable = GitSourceControl.GetProvider().GetGitVersion().bHasGitLfsLocking; 521 | // TODO LFS SRombauts : check if .gitattributes file is present and if Content/ is already tracked! 522 | const bool bGitAttributesCreated = true; 523 | return (bGitLfsLockingAvailable && (bAutoCreateGitAttributes || bGitAttributesCreated)); 524 | } 525 | 526 | FReply SGitSourceControlSettings::OnClickedInitializeGitRepository() 527 | { 528 | FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); 529 | const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath(); 530 | const FString PathToProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir()); 531 | TArray InfoMessages; 532 | TArray ErrorMessages; 533 | 534 | // 1.a. Synchronous (very quick) "git init" operation: initialize a Git local repository with a .git/ subdirectory 535 | GitSourceControlUtils::RunCommand(TEXT("init"), PathToGitBinary, PathToProjectDir, TArray(), TArray(), InfoMessages, ErrorMessages); 536 | // 1.b. Synchronous (very quick) "git remote add" operation: configure the URL of the default remote server 'origin' if specified 537 | if (!RemoteUrl.IsEmpty()) 538 | { 539 | TArray Parameters; 540 | Parameters.Add(TEXT("add origin")); 541 | Parameters.Add(RemoteUrl.ToString()); 542 | GitSourceControlUtils::RunCommand(TEXT("remote"), PathToGitBinary, PathToProjectDir, Parameters, TArray(), InfoMessages, ErrorMessages); 543 | } 544 | 545 | // Check the new repository status to enable connection (branch, user e-mail) 546 | GitSourceControl.GetProvider().CheckRepositoryStatus(PathToGitBinary); 547 | if (GitSourceControl.GetProvider().IsAvailable()) 548 | { 549 | // List of files to add to Source Control (.uproject, Config/, Content/, Source/ files and .gitignore/.gitattributes if any) 550 | TArray ProjectFiles; 551 | ProjectFiles.Add(FPaths::GetProjectFilePath()); 552 | ProjectFiles.Add(FPaths::ProjectConfigDir()); 553 | ProjectFiles.Add(FPaths::ProjectContentDir()); 554 | if (FPaths::DirectoryExists(FPaths::GameSourceDir())) 555 | { 556 | ProjectFiles.Add(FPaths::GameSourceDir()); 557 | } 558 | if (bAutoCreateGitIgnore) 559 | { 560 | // 2.a. Create a standard ".gitignore" file with common patterns for a typical Blueprint & C++ project 561 | const FString GitIgnoreFilename = FPaths::Combine(FPaths::ProjectDir(), TEXT(".gitignore")); 562 | const FString GitIgnoreContent = TEXT("Binaries\nBuild\nDerivedDataCache\nIntermediate\nSaved\nScript\nenc_temp_folder\n.idea\n.vscode\n.vs\n.vsconfig\n.ignore\n*.VC.db\n*.opensdf\n*.opendb\n*.sdf\n*.sln\n*.suo\n*.code-workspace\n*.xcodeproj\n*.xcworkspace\n*.log"); 563 | if (FFileHelper::SaveStringToFile(GitIgnoreContent, *GitIgnoreFilename, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) 564 | { 565 | ProjectFiles.Add(GitIgnoreFilename); 566 | } 567 | } 568 | if (bAutoCreateReadme) 569 | { 570 | // 2.b. Create a "README.md" file with a custom description 571 | const FString ReadmeFilename = FPaths::Combine(FPaths::ProjectDir(), TEXT("README.md")); 572 | if (FFileHelper::SaveStringToFile(ReadmeContent.ToString(), *ReadmeFilename, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) 573 | { 574 | ProjectFiles.Add(ReadmeFilename); 575 | } 576 | } 577 | if (bAutoCreateGitAttributes) 578 | { 579 | // 2.c. Synchronous (very quick) "lfs install" operation: needs only to be run once by user 580 | GitSourceControlUtils::RunCommand(TEXT("lfs install"), PathToGitBinary, PathToProjectDir, TArray(), TArray(), InfoMessages, ErrorMessages); 581 | 582 | // 2.d. Create a ".gitattributes" file to enable Git LFS (Large File System) for the whole "Content/" subdir 583 | const FString GitAttributesFilename = FPaths::Combine(FPaths::ProjectDir(), TEXT(".gitattributes")); 584 | FString GitAttributesContent; 585 | if (GitSourceControl.AccessSettings().IsUsingGitLfsLocking()) 586 | { 587 | // Git LFS 2.x File Locking mechanism 588 | GitAttributesContent = TEXT("Content/** filter=lfs diff=lfs merge=lfs -text lockable\n"); 589 | } 590 | else 591 | { 592 | GitAttributesContent = TEXT("Content/** filter=lfs diff=lfs merge=lfs -text\n"); 593 | } 594 | if (FFileHelper::SaveStringToFile(GitAttributesContent, *GitAttributesFilename, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) 595 | { 596 | ProjectFiles.Add(GitAttributesFilename); 597 | } 598 | } 599 | 600 | // 3. Add files to Source Control: launch an asynchronous MarkForAdd operation 601 | LaunchMarkForAddOperation(ProjectFiles); 602 | 603 | // 4. The CheckIn will follow, at completion of the MarkForAdd operation 604 | } 605 | return FReply::Handled(); 606 | } 607 | 608 | // Launch an asynchronous "MarkForAdd" operation and start an ongoing notification 609 | void SGitSourceControlSettings::LaunchMarkForAddOperation(const TArray& InFiles) 610 | { 611 | FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); 612 | TSharedRef MarkForAddOperation = ISourceControlOperation::Create(); 613 | ECommandResult::Type Result = GitSourceControl.GetProvider().Execute(MarkForAddOperation, InFiles, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateSP(this, &SGitSourceControlSettings::OnSourceControlOperationComplete)); 614 | if (Result == ECommandResult::Succeeded) 615 | { 616 | DisplayInProgressNotification(MarkForAddOperation); 617 | } 618 | else 619 | { 620 | DisplayFailureNotification(MarkForAddOperation); 621 | } 622 | } 623 | 624 | // Launch an asynchronous "CheckIn" operation and start another ongoing notification 625 | void SGitSourceControlSettings::LaunchCheckInOperation() 626 | { 627 | TSharedRef CheckInOperation = ISourceControlOperation::Create(); 628 | CheckInOperation->SetDescription(InitialCommitMessage); 629 | FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); 630 | ECommandResult::Type Result = GitSourceControl.GetProvider().Execute(CheckInOperation, TArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateSP(this, &SGitSourceControlSettings::OnSourceControlOperationComplete)); 631 | if (Result == ECommandResult::Succeeded) 632 | { 633 | DisplayInProgressNotification(CheckInOperation); 634 | } 635 | else 636 | { 637 | DisplayFailureNotification(CheckInOperation); 638 | } 639 | } 640 | 641 | /// Delegate called when a source control operation has completed: launch the next one and manage notifications 642 | void SGitSourceControlSettings::OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult) 643 | { 644 | RemoveInProgressNotification(); 645 | 646 | // Report result with a notification 647 | if (InResult == ECommandResult::Succeeded) 648 | { 649 | DisplaySuccessNotification(InOperation); 650 | } 651 | else 652 | { 653 | DisplayFailureNotification(InOperation); 654 | } 655 | 656 | if ((InOperation->GetName() == "MarkForAdd") && (InResult == ECommandResult::Succeeded) && bAutoInitialCommit) 657 | { 658 | // 4. optional initial Asynchronous commit with custom message: launch a "CheckIn" Operation 659 | LaunchCheckInOperation(); 660 | } 661 | } 662 | 663 | 664 | // Display an ongoing notification during the whole operation 665 | void SGitSourceControlSettings::DisplayInProgressNotification(const FSourceControlOperationRef& InOperation) 666 | { 667 | FNotificationInfo Info(InOperation->GetInProgressString()); 668 | Info.bFireAndForget = false; 669 | Info.ExpireDuration = 0.0f; 670 | Info.FadeOutDuration = 1.0f; 671 | OperationInProgressNotification = FSlateNotificationManager::Get().AddNotification(Info); 672 | if (OperationInProgressNotification.IsValid()) 673 | { 674 | OperationInProgressNotification.Pin()->SetCompletionState(SNotificationItem::CS_Pending); 675 | } 676 | } 677 | 678 | // Remove the ongoing notification at the end of the operation 679 | void SGitSourceControlSettings::RemoveInProgressNotification() 680 | { 681 | if (OperationInProgressNotification.IsValid()) 682 | { 683 | OperationInProgressNotification.Pin()->ExpireAndFadeout(); 684 | OperationInProgressNotification.Reset(); 685 | } 686 | } 687 | 688 | // Display a temporary success notification at the end of the operation 689 | void SGitSourceControlSettings::DisplaySuccessNotification(const FSourceControlOperationRef& InOperation) 690 | { 691 | const FText NotificationText = FText::Format(LOCTEXT("InitialCommit_Success", "{0} operation was successfull!"), FText::FromName(InOperation->GetName())); 692 | FNotificationInfo Info(NotificationText); 693 | Info.bUseSuccessFailIcons = true; 694 | #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1 695 | Info.Image = FAppStyle::GetBrush(TEXT("NotificationList.SuccessImage")); 696 | #else 697 | Info.Image = FEditorStyle::GetBrush(TEXT("NotificationList.SuccessImage")); 698 | #endif 699 | FSlateNotificationManager::Get().AddNotification(Info); 700 | } 701 | 702 | // Display a temporary failure notification at the end of the operation 703 | void SGitSourceControlSettings::DisplayFailureNotification(const FSourceControlOperationRef& InOperation) 704 | { 705 | const FText NotificationText = FText::Format(LOCTEXT("InitialCommit_Failure", "Error: {0} operation failed!"), FText::FromName(InOperation->GetName())); 706 | FNotificationInfo Info(NotificationText); 707 | Info.ExpireDuration = 8.0f; 708 | FSlateNotificationManager::Get().AddNotification(Info); 709 | } 710 | 711 | void SGitSourceControlSettings::OnCheckedCreateGitIgnore(ECheckBoxState NewCheckedState) 712 | { 713 | bAutoCreateGitIgnore = (NewCheckedState == ECheckBoxState::Checked); 714 | } 715 | 716 | void SGitSourceControlSettings::OnCheckedCreateReadme(ECheckBoxState NewCheckedState) 717 | { 718 | bAutoCreateReadme = (NewCheckedState == ECheckBoxState::Checked); 719 | } 720 | 721 | bool SGitSourceControlSettings::GetAutoCreateReadme() const 722 | { 723 | return bAutoCreateReadme; 724 | } 725 | 726 | void SGitSourceControlSettings::OnReadmeContentCommited(const FText& InText, ETextCommit::Type InCommitType) 727 | { 728 | ReadmeContent = InText; 729 | } 730 | 731 | FText SGitSourceControlSettings::GetReadmeContent() const 732 | { 733 | return ReadmeContent; 734 | } 735 | 736 | void SGitSourceControlSettings::OnCheckedCreateGitAttributes(ECheckBoxState NewCheckedState) 737 | { 738 | bAutoCreateGitAttributes = (NewCheckedState == ECheckBoxState::Checked); 739 | } 740 | 741 | void SGitSourceControlSettings::OnCheckedUseGitLfsLocking(ECheckBoxState NewCheckedState) 742 | { 743 | FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); 744 | GitSourceControl.AccessSettings().SetUsingGitLfsLocking(NewCheckedState == ECheckBoxState::Checked); 745 | GitSourceControl.AccessSettings().SaveSettings(); 746 | } 747 | 748 | bool SGitSourceControlSettings::GetIsUsingGitLfsLocking() const 749 | { 750 | const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); 751 | return GitSourceControl.AccessSettings().IsUsingGitLfsLocking(); 752 | } 753 | 754 | void SGitSourceControlSettings::OnIsPushAfterCommitEnabled(ECheckBoxState NewCheckedState) 755 | { 756 | FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); 757 | GitSourceControl.AccessSettings().SetIsPushAfterCommitEnabled(NewCheckedState == ECheckBoxState::Checked); 758 | GitSourceControl.AccessSettings().SaveSettings(); 759 | } 760 | 761 | bool SGitSourceControlSettings::GetIsPushAfterCommitEnabled() const 762 | { 763 | const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); 764 | return GitSourceControl.AccessSettings().IsPushAfterCommitEnabled(); 765 | } 766 | 767 | ECheckBoxState SGitSourceControlSettings::IsPushAfterCommitEnabled() const 768 | { 769 | return (GetIsPushAfterCommitEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked); 770 | } 771 | 772 | ECheckBoxState SGitSourceControlSettings::IsUsingGitLfsLocking() const 773 | { 774 | return (GetIsUsingGitLfsLocking() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked); 775 | } 776 | 777 | void SGitSourceControlSettings::OnLfsUserNameCommited(const FText& InText, ETextCommit::Type InCommitType) 778 | { 779 | FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); 780 | GitSourceControl.AccessSettings().SetLfsUserName(InText.ToString()); 781 | GitSourceControl.AccessSettings().SaveSettings(); 782 | } 783 | 784 | FText SGitSourceControlSettings::GetLfsUserName() const 785 | { 786 | const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); 787 | return FText::FromString(GitSourceControl.AccessSettings().GetLfsUserName()); 788 | } 789 | 790 | void SGitSourceControlSettings::OnCheckedInitialCommit(ECheckBoxState NewCheckedState) 791 | { 792 | bAutoInitialCommit = (NewCheckedState == ECheckBoxState::Checked); 793 | } 794 | 795 | bool SGitSourceControlSettings::GetAutoInitialCommit() const 796 | { 797 | return bAutoInitialCommit; 798 | } 799 | 800 | void SGitSourceControlSettings::OnInitialCommitMessageCommited(const FText& InText, ETextCommit::Type InCommitType) 801 | { 802 | InitialCommitMessage = InText; 803 | } 804 | 805 | FText SGitSourceControlSettings::GetInitialCommitMessage() const 806 | { 807 | return InitialCommitMessage; 808 | } 809 | 810 | void SGitSourceControlSettings::OnRemoteUrlCommited(const FText& InText, ETextCommit::Type InCommitType) 811 | { 812 | RemoteUrl = InText; 813 | } 814 | 815 | FText SGitSourceControlSettings::GetRemoteUrl() const 816 | { 817 | return RemoteUrl; 818 | } 819 | 820 | #undef LOCTEXT_NAMESPACE 821 | -------------------------------------------------------------------------------- /Source/GitSourceControl/Private/SGitSourceControlSettings.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 "Layout/Visibility.h" 10 | #include "Input/Reply.h" 11 | #include "Widgets/DeclarativeSyntaxSupport.h" 12 | #include "Widgets/SCompoundWidget.h" 13 | #include "SlateFwd.h" 14 | #include "ISourceControlOperation.h" 15 | #include "ISourceControlProvider.h" 16 | 17 | enum class ECheckBoxState : uint8; 18 | 19 | class SGitSourceControlSettings : public SCompoundWidget 20 | { 21 | public: 22 | 23 | SLATE_BEGIN_ARGS(SGitSourceControlSettings) {} 24 | 25 | SLATE_END_ARGS() 26 | 27 | public: 28 | 29 | void Construct(const FArguments& InArgs); 30 | 31 | ~SGitSourceControlSettings(); 32 | 33 | private: 34 | 35 | /** Delegates to get Git binary path from/to settings */ 36 | FString GetBinaryPathString() const; 37 | void OnBinaryPathPicked(const FString & PickedPath) const; 38 | 39 | /** Delegate to get repository root, user name and email from provider */ 40 | FText GetPathToRepositoryRoot() const; 41 | FText GetUserName() const; 42 | FText GetUserEmail() const; 43 | 44 | EVisibility MustInitializeGitRepository() const; 45 | bool CanInitializeGitRepository() const; 46 | bool CanInitializeGitLfs() const; 47 | bool CanUseGitLfsLocking() const; 48 | 49 | /** Delegate to initialize a new Git repository */ 50 | FReply OnClickedInitializeGitRepository(); 51 | 52 | void OnCheckedCreateGitIgnore(ECheckBoxState NewCheckedState); 53 | bool bAutoCreateGitIgnore; 54 | 55 | /** Delegates to create a README.md file */ 56 | void OnCheckedCreateReadme(ECheckBoxState NewCheckedState); 57 | bool GetAutoCreateReadme() const; 58 | bool bAutoCreateReadme; 59 | void OnReadmeContentCommited(const FText& InText, ETextCommit::Type InCommitType); 60 | FText GetReadmeContent() const; 61 | FText ReadmeContent; 62 | 63 | void OnCheckedCreateGitAttributes(ECheckBoxState NewCheckedState); 64 | bool bAutoCreateGitAttributes; 65 | 66 | void OnCheckedUseGitLfsLocking(ECheckBoxState NewCheckedState); 67 | ECheckBoxState IsUsingGitLfsLocking() const; 68 | bool GetIsUsingGitLfsLocking() const; 69 | 70 | void OnIsPushAfterCommitEnabled(ECheckBoxState NewCheckedState); 71 | bool GetIsPushAfterCommitEnabled() const; 72 | ECheckBoxState IsPushAfterCommitEnabled() const; 73 | 74 | void OnLfsUserNameCommited(const FText& InText, ETextCommit::Type InCommitType); 75 | FText GetLfsUserName() const; 76 | 77 | void OnCheckedInitialCommit(ECheckBoxState NewCheckedState); 78 | bool GetAutoInitialCommit() const; 79 | bool bAutoInitialCommit; 80 | void OnInitialCommitMessageCommited(const FText& InText, ETextCommit::Type InCommitType); 81 | FText GetInitialCommitMessage() const; 82 | FText InitialCommitMessage; 83 | 84 | void OnRemoteUrlCommited(const FText& InText, ETextCommit::Type InCommitType); 85 | FText GetRemoteUrl() const; 86 | FText RemoteUrl; 87 | 88 | /** Launch initial asynchronous add and commit operations */ 89 | void LaunchMarkForAddOperation(const TArray& InFiles); 90 | void LaunchCheckInOperation(); 91 | 92 | /** Delegate called when a source control operation has completed */ 93 | void OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult); 94 | 95 | /** Asynchronous operation progress notifications */ 96 | TWeakPtr OperationInProgressNotification; 97 | 98 | void DisplayInProgressNotification(const FSourceControlOperationRef& InOperation); 99 | void RemoveInProgressNotification(); 100 | void DisplaySuccessNotification(const FSourceControlOperationRef& InOperation); 101 | void DisplayFailureNotification(const FSourceControlOperationRef& InOperation); 102 | }; 103 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | show_downloads: true 2 | theme: jekyll-theme-slate --------------------------------------------------------------------------------