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