// Copyright Epic Games, Inc. All Rights Reserved. #include "LocalizationSourceControlUtil.h" #include "ISourceControlModule.h" #include "ISourceControlProvider.h" #include "SourceControlOperations.h" #include "SourceControlHelpers.h" #include "Misc/Paths.h" DEFINE_LOG_CATEGORY_STATIC(LogLocalizationSourceControl, Log, All); #define LOCTEXT_NAMESPACE "LocalizationSourceControl" FLocalizationSCC::FLocalizationSCC() { ISourceControlModule::Get().GetProvider().Init(); } FLocalizationSCC::~FLocalizationSCC() { if (CheckedOutFiles.Num() > 0) { UE_LOG(LogLocalizationSourceControl, Log, TEXT("Revision Control wrapper shutting down with checked out files.")); } ISourceControlModule::Get().GetProvider().Close(); } bool FLocalizationSCC::CheckOutFile(const FString& InFile, FText& OutError) { if (InFile.IsEmpty()) { OutError = LOCTEXT("InvalidFileSpecified", "Could not checkout file at invalid path."); return false; } if (InFile.StartsWith(TEXT("\\\\"))) { // We can't check out a UNC path, but don't say we failed return true; } FText SCCError; if (!IsReady(SCCError)) { OutError = SCCError; return false; } FString AbsoluteFilename = FPaths::ConvertRelativePathToFull(InFile); if (CheckedOutFiles.Contains(AbsoluteFilename)) { return true; } bool bSuccessfullyCheckedOut = false; TArray FilesToBeCheckedOut; FilesToBeCheckedOut.Add(AbsoluteFilename); FFormatNamedArguments Args; Args.Add(TEXT("Filepath"), FText::FromString(InFile)); ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(AbsoluteFilename, EStateCacheUsage::ForceUpdate); if (SourceControlState.IsValid() && SourceControlState->IsDeleted()) { // If it's deleted, we need to revert that first SourceControlProvider.Execute(ISourceControlOperation::Create(), FilesToBeCheckedOut); SourceControlState = SourceControlProvider.GetState(AbsoluteFilename, EStateCacheUsage::ForceUpdate); } if (SourceControlState.IsValid()) { FString Other; if (SourceControlState->IsAdded() || SourceControlState->IsCheckedOut()) { // Already checked out or opened for add bSuccessfullyCheckedOut = true; } else if (SourceControlState->CanCheckout()) { bSuccessfullyCheckedOut = (SourceControlProvider.Execute(ISourceControlOperation::Create(), FilesToBeCheckedOut) == ECommandResult::Succeeded); if (!bSuccessfullyCheckedOut) { OutError = FText::Format(LOCTEXT("FailedToCheckOutFile", "Failed to check out file '{Filepath}'."), Args); } } else if (!SourceControlState->IsSourceControlled() && SourceControlState->CanAdd()) { bSuccessfullyCheckedOut = (SourceControlProvider.Execute(ISourceControlOperation::Create(), FilesToBeCheckedOut) == ECommandResult::Succeeded); if (!bSuccessfullyCheckedOut) { OutError = FText::Format(LOCTEXT("FailedToAddFileToSourceControl", "Failed to add file '{Filepath}' to revision control."), Args); } } else if (!SourceControlState->IsCurrent()) { OutError = FText::Format(LOCTEXT("FileIsNotAtHeadRevision", "File '{Filepath}' is not at head revision."), Args); } else if (SourceControlState->IsCheckedOutOther(&(Other))) { Args.Add(TEXT("Username"), FText::FromString(Other)); OutError = FText::Format(LOCTEXT("FileIsAlreadyCheckedOutByAnotherUser", "File '{Filepath}' is checked out by another ('{Username}')."), Args); } else { // Improper or invalid SCC state OutError = FText::Format(LOCTEXT("CouldNotGetStateOfFile", "Could not determine revision control state of file '{Filepath}'."), Args); } } else { // Improper or invalid SCC state OutError = FText::Format(LOCTEXT("CouldNotGetStateOfFile", "Could not determine revision control state of file '{Filepath}'."), Args); } if (bSuccessfullyCheckedOut) { CheckedOutFiles.AddUnique(AbsoluteFilename); } return bSuccessfullyCheckedOut; } bool FLocalizationSCC::CheckinFiles(const FText& InChangeDescription, FText& OutError) { if (CheckedOutFiles.Num() == 0) { return true; } FText SCCError; if (!IsReady(SCCError)) { OutError = SCCError; return false; } ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); SourceControlHelpers::RevertUnchangedFiles(SourceControlProvider, CheckedOutFiles); // remove unchanged files for (int32 VerifyIndex = CheckedOutFiles.Num() - 1; VerifyIndex >= 0; --VerifyIndex) { FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(CheckedOutFiles[VerifyIndex], EStateCacheUsage::ForceUpdate); if (SourceControlState.IsValid() && !SourceControlState->IsCheckedOut() && !SourceControlState->IsAdded()) { CheckedOutFiles.RemoveAt(VerifyIndex); } } if (CheckedOutFiles.Num() > 0) { TSharedRef CheckInOperation = ISourceControlOperation::Create(); CheckInOperation->SetDescription(InChangeDescription); if (!SourceControlProvider.Execute(CheckInOperation, CheckedOutFiles)) { OutError = LOCTEXT("FailedToCheckInFiles", "The checked out localization files could not be checked in."); return false; } CheckedOutFiles.Empty(); } return true; } bool FLocalizationSCC::CleanUp(FText& OutError) { if (CheckedOutFiles.Num() == 0) { return true; } bool bCleanupSuccess = true; FString AccumulatedErrorsStr; const int32 FileCount = CheckedOutFiles.Num(); int32 FileIdx = 0; for (int32 i = 0; i < FileCount; ++i) { FText ErrorText; bool bReverted = RevertFile(CheckedOutFiles[FileIdx], ErrorText); bCleanupSuccess &= bReverted; if (!bReverted) { if (!AccumulatedErrorsStr.IsEmpty()) { AccumulatedErrorsStr += TEXT(", "); } AccumulatedErrorsStr += FString::Printf(TEXT("%s : %s"), *CheckedOutFiles[FileIdx], *ErrorText.ToString()); ++FileIdx; } } if (!bCleanupSuccess) { OutError = FText::Format(LOCTEXT("CouldNotCompleteSourceControlCleanup", "Could not complete Revision Control cleanup. {FailureReason}"), FText::FromString(AccumulatedErrorsStr)); } return bCleanupSuccess; } bool FLocalizationSCC::IsReady(FText& OutError) { if (!ISourceControlModule::Get().IsEnabled()) { OutError = LOCTEXT("SourceControlNotEnabled", "Revision control is not enabled."); return false; } if (!ISourceControlModule::Get().GetProvider().IsAvailable()) { OutError = LOCTEXT("SourceControlNotAvailable", "Revision control server is currently not available."); return false; } return true; } bool FLocalizationSCC::RevertFile(const FString& InFile, FText& OutError) { if (InFile.IsEmpty() || InFile.StartsWith(TEXT("\\\\"))) { OutError = LOCTEXT("CouldNotRevertFile", "Could not revert file."); return false; } FText SCCError; if (!IsReady(SCCError)) { OutError = SCCError; return false; } FString AbsoluteFilename = FPaths::ConvertRelativePathToFull(InFile); ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(AbsoluteFilename, EStateCacheUsage::ForceUpdate); bool bSuccessfullyReverted = false; TArray FilesToRevert; FilesToRevert.Add(AbsoluteFilename); if (SourceControlState.IsValid() && !SourceControlState->IsCheckedOut() && !SourceControlState->IsAdded()) { bSuccessfullyReverted = true; } else { bSuccessfullyReverted = (SourceControlProvider.Execute(ISourceControlOperation::Create(), FilesToRevert) == ECommandResult::Succeeded); } if (!bSuccessfullyReverted) { OutError = LOCTEXT("CouldNotRevertFile", "Could not revert file."); } else { CheckedOutFiles.Remove(AbsoluteFilename); } return bSuccessfullyReverted; } void FLocFileSCCNotifies::PreFileWrite(const FString& InFilename) { if (SourceControlInfo.IsValid() && FPaths::FileExists(InFilename)) { // File already exists, so check it out before writing to it FText ErrorMsg; if (!SourceControlInfo->CheckOutFile(InFilename, ErrorMsg)) { UE_LOG(LogLocalizationSourceControl, Error, TEXT("Failed to check out file '%s'. %s"), *InFilename, *ErrorMsg.ToString()); } } } void FLocFileSCCNotifies::PostFileWrite(const FString& InFilename) { if (SourceControlInfo.IsValid()) { // If the file didn't exist before then this will add it, otherwise it will do nothing FText ErrorMsg; if (!SourceControlInfo->CheckOutFile(InFilename, ErrorMsg)) { UE_LOG(LogLocalizationSourceControl, Error, TEXT("Failed to check out file '%s'. %s"), *InFilename, *ErrorMsg.ToString()); } } } #undef LOCTEXT_NAMESPACE