// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) // // Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt // or copy at http://opensource.org/licenses/MIT) #include "GitSourceControlOperations.h" #include "Misc/Paths.h" #include "Modules/ModuleManager.h" #include "SourceControlOperations.h" #include "ISourceControlModule.h" #include "GitSourceControlModule.h" #include "GitSourceControlCommand.h" #include "GitSourceControlUtils.h" #include "SourceControlHelpers.h" #include "Logging/MessageLog.h" #include "Misc/MessageDialog.h" #include "HAL/PlatformProcess.h" #include "GenericPlatform/GenericPlatformFile.h" #if ENGINE_MAJOR_VERSION >= 5 #include "HAL/PlatformFileManager.h" #else #include "HAL/PlatformFilemanager.h" #endif #include #define LOCTEXT_NAMESPACE "GitSourceControl" FName FGitConnectWorker::GetName() const { return "Connect"; } bool FGitConnectWorker::Execute(FGitSourceControlCommand& InCommand) { // The connect worker checks if we are connected to the remote server. check(InCommand.Operation->GetName() == GetName()); TSharedRef Operation = StaticCastSharedRef(InCommand.Operation); // Skip login operations, since Git does not have to login. // It's not a big deal for async commands though, so let those go through. // More information: this is a heuristic for cases where UE is trying to create // a valid Perforce connection as a side effect for the connect worker. For Git, // the connect worker has no side effects. It is simply a query to retrieve information // to be displayed to the user, like in the revision control settings or on init. // Therefore, there is no need for synchronously establishing a connection if not there. if (InCommand.Concurrency == EConcurrency::Synchronous) { InCommand.bCommandSuccessful = true; return true; } // Check Git availability // We already know that Git is available if PathToGitBinary is not empty, since it is validated then. if (InCommand.PathToGitBinary.IsEmpty()) { const FText& NotFound = LOCTEXT("GitNotFound", "Failed to enable Git revision control. You need to install Git and ensure the plugin has a valid path to the git executable."); InCommand.ResultInfo.ErrorMessages.Add(NotFound.ToString()); Operation->SetErrorText(NotFound); InCommand.bCommandSuccessful = false; return false; } // Get default branch: git remote show TArray Parameters { TEXT("-h"), // Only limit to branches TEXT("-q") // Skip printing out remote URL, we don't use it }; // Check if remote matches our refs. // Could be useful in the future, but all we want to know right now is if connection is up. // Parameters.Add("--exit-code"); TArray InfoMessages; TArray ErrorMessages; InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("ls-remote"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), FGitSourceControlModule::GetEmptyStringArray(), InfoMessages, ErrorMessages); if (!InCommand.bCommandSuccessful) { const FText& NotFound = LOCTEXT("GitRemoteFailed", "Failed Git remote connection. Ensure your repo is initialized, and check your connection to the Git host."); InCommand.ResultInfo.ErrorMessages.Add(NotFound.ToString()); Operation->SetErrorText(NotFound); } // TODO: always return true, and enter an offline mode if could not connect to remote return InCommand.bCommandSuccessful; } bool FGitConnectWorker::UpdateStates() const { return false; } FName FGitCheckOutWorker::GetName() const { return "CheckOut"; } bool FGitCheckOutWorker::Execute(FGitSourceControlCommand& InCommand) { // If we have nothing to process, exit immediately if (InCommand.Files.Num() == 0) { return true; } check(InCommand.Operation->GetName() == GetName()); if (!InCommand.bUsingGitLfsLocking) { InCommand.bCommandSuccessful = false; return InCommand.bCommandSuccessful; } // lock files: execute the LFS command on relative filenames const TArray& RelativeFiles = GitSourceControlUtils::RelativeFilenames(InCommand.Files, InCommand.PathToGitRoot); const TArray& LockableRelativeFiles = RelativeFiles.FilterByPredicate(GitSourceControlUtils::IsFileLFSLockable); if (LockableRelativeFiles.Num() < 1) { InCommand.bCommandSuccessful = true; return InCommand.bCommandSuccessful; } const bool bSuccess = GitSourceControlUtils::RunLFSCommand(TEXT("lock"), InCommand.PathToGitRoot, InCommand.PathToGitBinary, FGitSourceControlModule::GetEmptyStringArray(), LockableRelativeFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); InCommand.bCommandSuccessful = bSuccess; const FString& LockUser = FGitSourceControlModule::Get().GetProvider().GetLockUser(); if (bSuccess) { TArray AbsoluteFiles; for (const auto& RelativeFile : RelativeFiles) { FString AbsoluteFile = FPaths::Combine(InCommand.PathToGitRoot, RelativeFile); FGitLockedFilesCache::AddLockedFile(AbsoluteFile, LockUser); FPaths::NormalizeFilename(AbsoluteFile); AbsoluteFiles.Add(AbsoluteFile); } GitSourceControlUtils::CollectNewStates(AbsoluteFiles, States, EFileState::Unset, ETreeState::Unset, ELockState::Locked); for (auto& State : States) { State.Value.LockUser = LockUser; } } return InCommand.bCommandSuccessful; } bool FGitCheckOutWorker::UpdateStates() const { return GitSourceControlUtils::UpdateCachedStates(States); } static FText ParseCommitResults(const TArray& InResults) { if (InResults.Num() >= 1) { const FString& FirstLine = InResults[0]; return FText::Format(LOCTEXT("CommitMessage", "Commited {0}."), FText::FromString(FirstLine)); } return LOCTEXT("CommitMessageUnknown", "Submitted revision."); } FName FGitCheckInWorker::GetName() const { return "CheckIn"; } const FText EmptyCommitMsg; bool FGitCheckInWorker::Execute(FGitSourceControlCommand& InCommand) { check(InCommand.Operation->GetName() == GetName()); TSharedRef Operation = StaticCastSharedRef(InCommand.Operation); // make a temp file to place our commit message in bool bDoCommit = InCommand.Files.Num() > 0; const FText& CommitMsg = bDoCommit ? Operation->GetDescription() : EmptyCommitMsg; FGitScopedTempFile CommitMsgFile(CommitMsg); if (CommitMsgFile.GetFilename().Len() > 0) { FGitSourceControlProvider& Provider = FGitSourceControlModule::Get().GetProvider(); if (bDoCommit) { FString ParamCommitMsgFilename = TEXT("--file=\""); ParamCommitMsgFilename += FPaths::ConvertRelativePathToFull(CommitMsgFile.GetFilename()); ParamCommitMsgFilename += TEXT("\""); TArray CommitParameters {ParamCommitMsgFilename}; const TArray& FilesToCommit = GitSourceControlUtils::RelativeFilenames(InCommand.Files, InCommand.PathToRepositoryRoot); // If no files were committed, this is false, so we treat it as if we never wanted to commit in the first place. bDoCommit = GitSourceControlUtils::RunCommit(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, CommitParameters, FilesToCommit, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); } // If we commit, we can push up the deleted state to gone if (bDoCommit) { // Remove any deleted files from status cache TArray> LocalStates; Provider.GetState(InCommand.Files, LocalStates, EStateCacheUsage::Use); for (const auto& State : LocalStates) { if (State->IsDeleted()) { Provider.RemoveFileFromCache(State->GetFilename()); } } Operation->SetSuccessMessage(ParseCommitResults(InCommand.ResultInfo.InfoMessages)); const FString& Message = (InCommand.ResultInfo.InfoMessages.Num() > 0) ? InCommand.ResultInfo.InfoMessages[0] : TEXT(""); UE_LOG(LogSourceControl, Log, TEXT("commit successful: %s"), *Message); GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary); } // Collect difference between the remote and what we have on top of remote locally. This is to handle unpushed commits other than the one we just did. // Doesn't matter that we're not synced. Because our local branch is always based on the remote. TArray CommittedFiles; FString BranchName; bool bDiffSuccess; if (GitSourceControlUtils::GetRemoteBranchName(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, BranchName)) { TArray Parameters {"--name-only", FString::Printf(TEXT("%s...HEAD"), *BranchName), "--"}; bDiffSuccess = GitSourceControlUtils::RunCommand(TEXT("diff"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters, FGitSourceControlModule::GetEmptyStringArray(), CommittedFiles, InCommand.ResultInfo.ErrorMessages); } else { // Get all non-remote commits and list out their files TArray Parameters {"--branches", "--not" "--remotes", "--name-only", "--pretty="}; bDiffSuccess = GitSourceControlUtils::RunCommand(TEXT("log"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters, FGitSourceControlModule::GetEmptyStringArray(), CommittedFiles, InCommand.ResultInfo.ErrorMessages); // Dedup files list between commits CommittedFiles = TSet{CommittedFiles}.Array(); } bool bUnpushedFiles; TSet FilesToCheckIn {InCommand.Files}; if (bDiffSuccess) { // Only push if we have a difference (any commits at all, not just the one we just did) bUnpushedFiles = CommittedFiles.Num() > 0; CommittedFiles = GitSourceControlUtils::AbsoluteFilenames(CommittedFiles, InCommand.PathToRepositoryRoot); FilesToCheckIn.Append(CommittedFiles.FilterByPredicate(GitSourceControlUtils::IsFileLFSLockable)); } else { // Be cautious, try pushing anyway bUnpushedFiles = true; } TArray PulledFiles; // If we have unpushed files, push if (bUnpushedFiles) { // TODO: configure remote TArray PushParameters {TEXT("-u"), TEXT("origin"), TEXT("HEAD")}; InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("push"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, PushParameters, FGitSourceControlModule::GetEmptyStringArray(), InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); if (!InCommand.bCommandSuccessful) { // if out of date, pull first, then try again bool bWasOutOfDate = false; for (const auto& PushError : InCommand.ResultInfo.ErrorMessages) { if ((PushError.Contains(TEXT("[rejected]")) && (PushError.Contains(TEXT("non-fast-forward")) || PushError.Contains(TEXT("fetch first")))) || PushError.Contains(TEXT("cannot lock ref"))) { // Don't do it during iteration, want to append pull results to InCommand.ResultInfo.ErrorMessages bWasOutOfDate = true; break; } } if (bWasOutOfDate) { // Get latest const bool bFetched = GitSourceControlUtils::FetchRemote(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, false, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); if (bFetched) { // Update local with latest const bool bPulled = GitSourceControlUtils::PullOrigin(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), PulledFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); if (bPulled) { InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand( TEXT("push"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, PushParameters, FGitSourceControlModule::GetEmptyStringArray(), InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); } } // Our push still wasn't successful if (!InCommand.bCommandSuccessful) { if (!Provider.bPendingRestart) { // If it fails, just let the user do it FText PushFailMessage(LOCTEXT("GitPush_OutOfDate_Msg", "Git Push failed because there are changes you need to pull.\n\n" "An attempt was made to pull, but failed, because while the Unreal Editor is " "open, files cannot always be updated.\n\n" "Please exit the editor, and update the project again.")); FText PushFailTitle(LOCTEXT("GitPush_OutOfDate_Title", "Git Pull Required")); #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 FMessageDialog::Open(EAppMsgType::Ok, PushFailMessage, PushFailTitle); #else FMessageDialog::Open(EAppMsgType::Ok, PushFailMessage, &PushFailTitle); #endif UE_LOG(LogSourceControl, Log, TEXT("Push failed because we're out of date, prompting user to resolve manually")); } } } } } else { InCommand.bCommandSuccessful = true; } // git-lfs: unlock files if (InCommand.bUsingGitLfsLocking) { // If we successfully pushed (or didn't need to push), unlock the files marked for check in if (InCommand.bCommandSuccessful) { // unlock files: execute the LFS command on relative filenames // (unlock only locked files, that is, not Added files) TArray LockedFiles; GitSourceControlUtils::GetLockedFiles(FilesToCheckIn.Array(), LockedFiles); if (LockedFiles.Num() > 0) { const TArray& FilesToUnlock = GitSourceControlUtils::RelativeFilenames(LockedFiles, InCommand.PathToGitRoot); if (FilesToUnlock.Num() > 0) { // Not strictly necessary to succeed, so don't update command success const bool bUnlockSuccess = GitSourceControlUtils::RunLFSCommand(TEXT("unlock"), InCommand.PathToGitRoot, InCommand.PathToGitBinary, FGitSourceControlModule::GetEmptyStringArray(), FilesToUnlock, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); if (bUnlockSuccess) { for (const auto& File : LockedFiles) { FGitLockedFilesCache::RemoveLockedFile(File); } } } } #if 0 for (const FString& File : FilesToCheckIn.Array()) { FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*File, true); } #endif } } // Collect all the files we touched through the pull update if (bUnpushedFiles && PulledFiles.Num()) { FilesToCheckIn.Append(PulledFiles); } // Before, we added only lockable files from CommittedFiles. But now, we want to update all files, not just lockables. FilesToCheckIn.Append(CommittedFiles); // now update the status of our files TMap UpdatedStates; bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, FilesToCheckIn.Array(), InCommand.ResultInfo.ErrorMessages, UpdatedStates); if (bSuccess) { GitSourceControlUtils::CollectNewStates(UpdatedStates, States); } GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository")); return InCommand.bCommandSuccessful; } InCommand.bCommandSuccessful = false; return false; } bool FGitCheckInWorker::UpdateStates() const { return GitSourceControlUtils::UpdateCachedStates(States); } FName FGitMarkForAddWorker::GetName() const { return "MarkForAdd"; } bool FGitMarkForAddWorker::Execute(FGitSourceControlCommand& InCommand) { // If we have nothing to process, exit immediately if (InCommand.Files.Num() == 0) { return true; } check(InCommand.Operation->GetName() == GetName()); InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), InCommand.Files, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); if (InCommand.bCommandSuccessful) { GitSourceControlUtils::CollectNewStates(InCommand.Files, States, EFileState::Added, ETreeState::Staged); } else { TMap UpdatedStates; bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates); if (bSuccess) { GitSourceControlUtils::CollectNewStates(UpdatedStates, States); } GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository")); } return InCommand.bCommandSuccessful; } bool FGitMarkForAddWorker::UpdateStates() const { return GitSourceControlUtils::UpdateCachedStates(States); } FName FGitDeleteWorker::GetName() const { return "Delete"; } bool FGitDeleteWorker::Execute(FGitSourceControlCommand& InCommand) { // If we have nothing to process, exit immediately if (InCommand.Files.Num() == 0) { return true; } check(InCommand.Operation->GetName() == GetName()); InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("rm"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), InCommand.Files, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); if (InCommand.bCommandSuccessful) { GitSourceControlUtils::CollectNewStates(InCommand.Files, States, EFileState::Deleted, ETreeState::Staged); } else { TMap UpdatedStates; bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates); if (bSuccess) { GitSourceControlUtils::CollectNewStates(UpdatedStates, States); } GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository")); } return InCommand.bCommandSuccessful; } bool FGitDeleteWorker::UpdateStates() const { return GitSourceControlUtils::UpdateCachedStates(States); } // Get lists of Missing files (ie "deleted"), Modified files, and "other than Added" Existing files void GetMissingVsExistingFiles(const TArray& InFiles, TArray& OutMissingFiles, TArray& OutAllExistingFiles, TArray& OutOtherThanAddedExistingFiles) { FGitSourceControlModule& GitSourceControl = FGitSourceControlModule::Get(); FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); const TArray Files = (InFiles.Num() > 0) ? (InFiles) : (Provider.GetFilesInCache()); TArray> LocalStates; Provider.GetState(Files, LocalStates, EStateCacheUsage::Use); for (const auto& State : LocalStates) { if (FPaths::FileExists(State->GetFilename())) { if (State->IsAdded()) { OutAllExistingFiles.Add(State->GetFilename()); } else if (State->IsModified()) { OutOtherThanAddedExistingFiles.Add(State->GetFilename()); OutAllExistingFiles.Add(State->GetFilename()); } else if (State->CanRevert()) // for locked but unmodified files { OutOtherThanAddedExistingFiles.Add(State->GetFilename()); } } else { // If already queued for deletion, don't try to delete again if (State->IsSourceControlled() && !State->IsDeleted()) { OutMissingFiles.Add(State->GetFilename()); } } } } FName FGitRevertWorker::GetName() const { return "Revert"; } bool FGitRevertWorker::Execute(FGitSourceControlCommand& InCommand) { InCommand.bCommandSuccessful = true; // Filter files by status TArray MissingFiles; TArray AllExistingFiles; TArray OtherThanAddedExistingFiles; GetMissingVsExistingFiles(InCommand.Files, MissingFiles, AllExistingFiles, OtherThanAddedExistingFiles); const bool bRevertAll = InCommand.Files.Num() < 1; if (bRevertAll) { TArray Parms; Parms.Add(TEXT("--hard")); InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("reset"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parms, FGitSourceControlModule::GetEmptyStringArray(), InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); Parms.Reset(2); Parms.Add(TEXT("-f")); // force Parms.Add(TEXT("-d")); // remove directories InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("clean"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parms, FGitSourceControlModule::GetEmptyStringArray(), InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); } else { if (MissingFiles.Num() > 0) { // "Added" files that have been deleted needs to be removed from revision control InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("rm"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), MissingFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); } if (AllExistingFiles.Num() > 0) { // reset and revert any changes already added to the index InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("reset"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), AllExistingFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("checkout"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), AllExistingFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); } if (OtherThanAddedExistingFiles.Num() > 0) { // revert any changes in working copy (this would fails if the asset was in "Added" state, since after "reset" it is now "untracked") // may need to try a few times due to file locks from prior operations bool CheckoutSuccess = false; int32 Attempts = 10; while( Attempts-- > 0 ) { CheckoutSuccess = GitSourceControlUtils::RunCommand(TEXT("checkout"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), OtherThanAddedExistingFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); if (CheckoutSuccess) { break; } FPlatformProcess::Sleep(0.1f); } InCommand.bCommandSuccessful &= CheckoutSuccess; } } if (InCommand.bUsingGitLfsLocking) { // unlock files: execute the LFS command on relative filenames // (unlock only locked files, that is, not Added files) TArray LockedFiles; GitSourceControlUtils::GetLockedFiles(OtherThanAddedExistingFiles, LockedFiles); if (LockedFiles.Num() > 0) { const TArray& RelativeFiles = GitSourceControlUtils::RelativeFilenames(LockedFiles, InCommand.PathToGitRoot); InCommand.bCommandSuccessful &= GitSourceControlUtils::RunLFSCommand(TEXT("unlock"), InCommand.PathToGitRoot, InCommand.PathToGitBinary, FGitSourceControlModule::GetEmptyStringArray(), RelativeFiles, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); if (InCommand.bCommandSuccessful) { for (const auto& File : LockedFiles) { FGitLockedFilesCache::RemoveLockedFile(File); } } } } // If no files were specified (full revert), refresh all relevant files instead of the specified files (which is an empty list in full revert) // This is required so that files that were "Marked for add" have their status updated after a full revert. TArray FilesToUpdate = InCommand.Files; if (InCommand.Files.Num() <= 0) { for (const auto& File : MissingFiles) FilesToUpdate.Add(File); for (const auto& File : AllExistingFiles) FilesToUpdate.Add(File); for (const auto& File : OtherThanAddedExistingFiles) FilesToUpdate.Add(File); } // now update the status of our files TMap UpdatedStates; bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, FilesToUpdate, InCommand.ResultInfo.ErrorMessages, UpdatedStates); if (bSuccess) { GitSourceControlUtils::CollectNewStates(UpdatedStates, States); } GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository")); return InCommand.bCommandSuccessful; } bool FGitRevertWorker::UpdateStates() const { return GitSourceControlUtils::UpdateCachedStates(States); } FName FGitSyncWorker::GetName() const { return "Sync"; } bool FGitSyncWorker::Execute(FGitSourceControlCommand& InCommand) { TArray Results; const bool bFetched = GitSourceControlUtils::FetchRemote(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, false, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); if (!bFetched) { return false; } InCommand.bCommandSuccessful = GitSourceControlUtils::PullOrigin(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.Files, InCommand.Files, Results, InCommand.ResultInfo.ErrorMessages); // now update the status of our files TMap UpdatedStates; const bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates); if (bSuccess) { GitSourceControlUtils::CollectNewStates(UpdatedStates, States); } GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository")); GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary); return InCommand.bCommandSuccessful; } bool FGitSyncWorker::UpdateStates() const { return GitSourceControlUtils::UpdateCachedStates(States); } FName FGitFetch::GetName() const { return "Fetch"; } FText FGitFetch::GetInProgressString() const { // TODO Configure origin return LOCTEXT("SourceControl_Push", "Fetching from remote origin..."); } FName FGitFetchWorker::GetName() const { return "Fetch"; } bool FGitFetchWorker::Execute(FGitSourceControlCommand& InCommand) { InCommand.bCommandSuccessful = GitSourceControlUtils::FetchRemote(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); if (!InCommand.bCommandSuccessful) { return false; } check(InCommand.Operation->GetName() == GetName()); TSharedRef Operation = StaticCastSharedRef(InCommand.Operation); if (Operation->bUpdateStatus) { // Now update the status of all our files const TArray ProjectDirs {FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()),FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir()), FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath())}; TMap UpdatedStates; InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, ProjectDirs, InCommand.ResultInfo.ErrorMessages, UpdatedStates); GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository")); if (InCommand.bCommandSuccessful) { GitSourceControlUtils::CollectNewStates(UpdatedStates, States); } } return InCommand.bCommandSuccessful; } bool FGitFetchWorker::UpdateStates() const { return GitSourceControlUtils::UpdateCachedStates(States); } FName FGitUpdateStatusWorker::GetName() const { return "UpdateStatus"; } bool FGitUpdateStatusWorker::Execute(FGitSourceControlCommand& InCommand) { check(InCommand.Operation->GetName() == GetName()); TSharedRef Operation = StaticCastSharedRef(InCommand.Operation); if(InCommand.Files.Num() > 0) { TMap UpdatedStates; InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates); GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository")); if (InCommand.bCommandSuccessful) { GitSourceControlUtils::CollectNewStates(UpdatedStates, States); if (Operation->ShouldUpdateHistory()) { for (const auto& State : UpdatedStates) { const FString& File = State.Key; TGitSourceControlHistory History; if (State.Value.IsConflicted()) { // In case of a merge conflict, we first need to get the tip of the "remote branch" (MERGE_HEAD) GitSourceControlUtils::RunGetHistory(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, File, true, InCommand.ResultInfo.ErrorMessages, History); } // Get the history of the file in the current branch InCommand.bCommandSuccessful &= GitSourceControlUtils::RunGetHistory(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, File, false, InCommand.ResultInfo.ErrorMessages, History); Histories.Add(*File, History); } } } } else { // no path provided: only update the status of assets in Content/ directory and also Config files const TArray ProjectDirs {FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()), FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir()), FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath())}; TMap UpdatedStates; InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, ProjectDirs, InCommand.ResultInfo.ErrorMessages, UpdatedStates); GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository")); if (InCommand.bCommandSuccessful) { GitSourceControlUtils::CollectNewStates(UpdatedStates, States); } } GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary); // don't use the ShouldUpdateModifiedState() hint here as it is specific to Perforce: the above normal Git status has already told us this information (like Git and Mercurial) return InCommand.bCommandSuccessful; } bool FGitUpdateStatusWorker::UpdateStates() const { bool bUpdated = GitSourceControlUtils::UpdateCachedStates(States); FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked( "GitSourceControl" ); FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); const bool bUsingGitLfsLocking = Provider.UsesCheckout(); // TODO without LFS : Workaround a bug with the Source Control Module not updating file state after a simple "Save" with no "Checkout" (when not using File Lock) const FDateTime Now = bUsingGitLfsLocking ? FDateTime::Now() : FDateTime::MinValue(); // add history, if any for(const auto& History : Histories) { TSharedRef State = Provider.GetStateInternal(History.Key); State->History = History.Value; State->TimeStamp = Now; bUpdated = true; } return bUpdated; } FName FGitCopyWorker::GetName() const { return "Copy"; } bool FGitCopyWorker::Execute(FGitSourceControlCommand& InCommand) { check(InCommand.Operation->GetName() == GetName()); // Copy or Move operation on a single file : Git does not need an explicit copy nor move, // but after a Move the Editor create a redirector file with the old asset name that points to the new asset. // The redirector needs to be committed with the new asset to perform a real rename. // => the following is to "MarkForAdd" the redirector, but it still need to be committed by selecting the whole directory and "check-in" InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), InCommand.Files, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); if (InCommand.bCommandSuccessful) { GitSourceControlUtils::CollectNewStates(InCommand.Files, States, EFileState::Added, ETreeState::Staged); } else { TMap UpdatedStates; const bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates); GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository")); if (bSuccess) { GitSourceControlUtils::CollectNewStates(UpdatedStates, States); } } return InCommand.bCommandSuccessful; } bool FGitCopyWorker::UpdateStates() const { return GitSourceControlUtils::UpdateCachedStates(States); } FName FGitResolveWorker::GetName() const { return "Resolve"; } bool FGitResolveWorker::Execute( class FGitSourceControlCommand& InCommand ) { check(InCommand.Operation->GetName() == GetName()); // mark the conflicting files as resolved: TArray Results; InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), InCommand.Files, Results, InCommand.ResultInfo.ErrorMessages); // now update the status of our files TMap UpdatedStates; const bool bSuccess = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.ErrorMessages, UpdatedStates); GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository")); if (bSuccess) { GitSourceControlUtils::CollectNewStates(UpdatedStates, States); } return InCommand.bCommandSuccessful; } bool FGitResolveWorker::UpdateStates() const { return GitSourceControlUtils::UpdateCachedStates(States); } FName FGitMoveToChangelistWorker::GetName() const { return "MoveToChangelist"; } bool FGitMoveToChangelistWorker::UpdateStates() const { return true; } bool FGitMoveToChangelistWorker::Execute(FGitSourceControlCommand& InCommand) { check(InCommand.Operation->GetName() == GetName()); FGitSourceControlChangelist DestChangelist = InCommand.Changelist; bool bResult = false; if(DestChangelist.GetName().Equals(TEXT("Staged"))) { bResult = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, FGitSourceControlModule::GetEmptyStringArray(), InCommand.Files, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); } else if(DestChangelist.GetName().Equals(TEXT("Working"))) { TArray Parameter; Parameter.Add(TEXT("--staged")); bResult = GitSourceControlUtils::RunCommand(TEXT("restore"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameter, InCommand.Files, InCommand.ResultInfo.InfoMessages, InCommand.ResultInfo.ErrorMessages); } if (bResult) { TMap DummyStates; GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ResultInfo.InfoMessages, DummyStates); } return bResult; } FName FGitUpdateStagingWorker::GetName() const { return "UpdateChangelistsStatus"; } bool FGitUpdateStagingWorker::Execute(FGitSourceControlCommand& InCommand) { return GitSourceControlUtils::UpdateChangelistStateByCommand(); } bool FGitUpdateStagingWorker::UpdateStates() const { return true; } #undef LOCTEXT_NAMESPACE