You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/LocalFileSCComponent.cpp

495 lines
19 KiB
C++

/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "AzToolsFramework_precompiled.h"
#include <AzToolsFramework/SourceControl/LocalFileSCComponent.h>
#include <AzCore/Component/TickBus.h>
#include <AzCore/Jobs/JobFunction.h>
#include <AzCore/IO/SystemFile.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/string/regex.h>
#include <AzCore/StringFunc/StringFunc.h>
AZ_PUSH_DISABLE_WARNING(4251, "-Wunknown-warning-option") // 4251: 'QFileInfo::d_ptr': class 'QSharedDataPointer<QFileInfoPrivate>' needs to have dll-interface to be used by clients of class 'QFileInfo'
#include <QDir>
#include <QDirIterator>
AZ_POP_DISABLE_WARNING
namespace AzToolsFramework
{
void RefreshInfoFromFileSystem(SourceControlFileInfo& fileInfo)
{
fileInfo.m_flags = 0;
if (!AZ::IO::SystemFile::Exists(fileInfo.m_filePath.c_str()))
{
fileInfo.m_flags |= SCF_Writeable; // Non-existent files are not read only
}
else
{
fileInfo.m_flags |= SCF_Tracked;
if (AZ::IO::SystemFile::IsWritable(fileInfo.m_filePath.c_str()))
{
fileInfo.m_flags |= SCF_Writeable | SCF_OpenByUser;
}
}
fileInfo.m_status = SCS_OpSuccess;
}
void RemoveReadOnly(const SourceControlFileInfo& fileInfo)
{
if (!fileInfo.HasFlag(SCF_Writeable) && fileInfo.HasFlag(SCF_Tracked))
{
AZ::IO::SystemFile::SetWritable(fileInfo.m_filePath.c_str(), true);
}
}
void LocalFileSCComponent::Activate()
{
SourceControlConnectionRequestBus::Handler::BusConnect();
SourceControlCommandBus::Handler::BusConnect();
}
void LocalFileSCComponent::Deactivate()
{
SourceControlCommandBus::Handler::BusDisconnect();
SourceControlConnectionRequestBus::Handler::BusDisconnect();
}
void LocalFileSCComponent::GetFileInfo(const char* fullFilePath, const SourceControlResponseCallback& respCallback)
{
SourceControlFileInfo fileInfo(fullFilePath);
auto job = AZ::CreateJobFunction([fileInfo, respCallback]() mutable
{
RefreshInfoFromFileSystem(fileInfo);
AZ::TickBus::QueueFunction(respCallback, fileInfo.CompareStatus(SCS_OpSuccess), fileInfo);
}, true);
job->Start();
}
void LocalFileSCComponent::GetBulkFileInfo(const AZStd::unordered_set<AZStd::string>& fullFilePaths, const SourceControlResponseCallbackBulk& respCallback)
{
auto job = AZ::CreateJobFunction([fullFilePaths, respCallback]() mutable
{
AZStd::vector<SourceControlFileInfo> fileInfo;
for (const AZStd::string& fullFilePath : fullFilePaths)
{
for (QString file : GetFiles(fullFilePath.c_str()))
{
fileInfo.push_back();
fileInfo.back().m_filePath = file.toUtf8().constData();
RefreshInfoFromFileSystem(fileInfo.back());
}
}
AZ::TickBus::QueueFunction(respCallback, true, fileInfo);
}, true);
job->Start();
}
void LocalFileSCComponent::RequestEdit(const char* fullFilePath, bool /*allowMultiCheckout*/, const SourceControlResponseCallback& respCallback)
{
SourceControlFileInfo fileInfo(fullFilePath);
auto job = AZ::CreateJobFunction([fileInfo, respCallback]() mutable
{
RefreshInfoFromFileSystem(fileInfo);
RemoveReadOnly(fileInfo);
RefreshInfoFromFileSystem(fileInfo);
// As a quality of life improvement for our users, we want request edit
// to report success in the case where a file doesn't exist. We do this so
// developers can always call RequestEdit before a save operation; instead of:
// File Exists --> RequestEdit, then SaveOperation
// File Does not Exist --> SaveOperation, then RequestEdit
AZ::TickBus::QueueFunction(respCallback, fileInfo.HasFlag(SCF_Writeable), fileInfo);
}, true);
job->Start();
}
void LocalFileSCComponent::RequestEditBulk(const AZStd::unordered_set<AZStd::string>& fullFilePaths, bool /*allowMultiCheckout*/, const SourceControlResponseCallbackBulk& respCallback)
{
auto job = AZ::CreateJobFunction([fullFilePaths, respCallback]() mutable
{
AZStd::vector<SourceControlFileInfo> info;
for (const auto& filePath : fullFilePaths)
{
info.push_back();
auto& fileInfo = info.back();
fileInfo.m_filePath = filePath;
RefreshInfoFromFileSystem(fileInfo);
RemoveReadOnly(fileInfo);
RefreshInfoFromFileSystem(fileInfo);
}
// As a quality of life improvement for our users, we want request edit
// to report success in the case where a file doesn't exist. We do this so
// developers can always call RequestEdit before a save operation; instead of:
// File Exists --> RequestEdit, then SaveOperation
// File Does not Exist --> SaveOperation, then RequestEdit
AZ::TickBus::QueueFunction(respCallback, true, info);
}, true);
job->Start();
}
void LocalFileSCComponent::RequestDelete(const char* fullFilePath, const SourceControlResponseCallback& respCallback)
{
RequestDeleteExtended(fullFilePath, false, respCallback);
}
void LocalFileSCComponent::RequestDeleteExtended(const char* fullFilePath, bool skipReadOnly, const SourceControlResponseCallback& respCallback)
{
SourceControlFileInfo fileInfo(fullFilePath);
auto job = AZ::CreateJobFunction([fileInfo, skipReadOnly, respCallback]() mutable
{
RefreshInfoFromFileSystem(fileInfo);
if (!skipReadOnly)
{
RemoveReadOnly(fileInfo);
}
auto succeeded = AZ::IO::SystemFile::Delete(fileInfo.m_filePath.c_str());
RefreshInfoFromFileSystem(fileInfo);
AZ::TickBus::QueueFunction(respCallback, succeeded, fileInfo);
}, true);
job->Start();
}
bool LocalFileSCComponent::SplitWildcardPath(QString path, QString& root, QString& wildcardEntry, QString& remaining)
{
static constexpr char WildcardCharacter = '*';
static constexpr char RecursiveWildcard[] = "...";
int firstWildcardIndex = path.indexOf(WildcardCharacter);
if(firstWildcardIndex < 0)
{
firstWildcardIndex = path.indexOf(RecursiveWildcard);
}
if(firstWildcardIndex >= 0)
{
int lastSlashBeforeWildcard = path.lastIndexOf(AZ_CORRECT_FILESYSTEM_SEPARATOR, firstWildcardIndex);
root = path.left(lastSlashBeforeWildcard + 1); // Include the separator
wildcardEntry = path.mid(lastSlashBeforeWildcard + 1); // Skip the separator
remaining = "";
int nextSlashAfterWildcard = wildcardEntry.indexOf(AZ_CORRECT_FILESYSTEM_SEPARATOR);
if(nextSlashAfterWildcard >= 0)
{
remaining = wildcardEntry.mid(nextSlashAfterWildcard + 1); // Skip the separator
wildcardEntry = wildcardEntry.left(nextSlashAfterWildcard); // Skip the separator
}
return true;
}
return false;
}
QStringList RecurseAllFiles(QString path)
{
QDirIterator itr(path, QDir::Files | QDir::NoSymLinks | QDir::Hidden, QDirIterator::Subdirectories);
QStringList result;
while(itr.hasNext())
{
result += itr.next();
}
return result;
}
bool LocalFileSCComponent::ResolveOneWildcardLevel(QString search, QStringList& result)
{
QString root;
QString searchEntry;
QString remaining;
if(SplitWildcardPath(search, root, searchEntry, remaining))
{
bool recursive = searchEntry.endsWith("...");
searchEntry = searchEntry.replace("...", "*");
QDir searchRoot(root);
QStringList nameFilters;
nameFilters << searchEntry;
QDir::Filters dirFilters = remaining.isEmpty() ? QDir::Files : QDir::Dirs;
bool searchEverything = recursive && remaining.isEmpty();
QStringList files = searchRoot.entryList(nameFilters, dirFilters | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden);
for (QString& file : files)
{
file = root + file;
if (!remaining.isEmpty())
{
file += AZ_CORRECT_FILESYSTEM_SEPARATOR + remaining;
}
}
result += files;
if(searchEverything)
{
for (QString& file : searchRoot.entryList(nameFilters, QDir::Dirs | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden))
{
result += RecurseAllFiles(root + file);
}
}
return !remaining.isEmpty();
}
if(AZ::IO::SystemFile::Exists(search.toUtf8().constData()))
{
result = QStringList{ search };
}
else
{
result = QStringList{};
}
return !remaining.isEmpty();
}
QStringList LocalFileSCComponent::GetFiles(QString absolutePathQuery)
{
AZStd::string azPath = absolutePathQuery.toUtf8().constData();
AZ::StringFunc::Replace(azPath, AZ_WRONG_FILESYSTEM_SEPARATOR, AZ_CORRECT_FILESYSTEM_SEPARATOR);
QString remaining;
QStringList entries;
if(!ResolveOneWildcardLevel(azPath.c_str(), entries))
{
return entries;
}
QStringList results;
for (QString entry : entries)
{
results += GetFiles(entry);
}
return results;
}
AZStd::string LocalFileSCComponent::ResolveWildcardDestination(AZStd::string_view absFile, AZStd::string_view absSearch, AZStd::string destination)
{
AZStd::string searchAsRegex = absSearch;
AZ::StringFunc::Replace(searchAsRegex, AZ_WRONG_FILESYSTEM_SEPARATOR, AZ_CORRECT_FILESYSTEM_SEPARATOR);
AZ::StringFunc::Replace(destination, AZ_WRONG_FILESYSTEM_SEPARATOR, AZ_CORRECT_FILESYSTEM_SEPARATOR);
AZ::StringFunc::Replace(searchAsRegex, "...", "*");
AZ::StringFunc::Replace(destination, "...", "*");
AZStd::regex specialCharacters(R"([\\.?^$+(){}[\]-])");
// Escape the regex special characters
searchAsRegex = AZStd::regex_replace(searchAsRegex, specialCharacters, R"(\$0)");
// Replace * with .*
searchAsRegex = AZStd::regex_replace(searchAsRegex, AZStd::regex(R"(\*)"), R"((.*))");
AZStd::smatch result;
// Match absSearch against absFile to find what each * expands to
if (AZStd::regex_search(absFile.begin(), absFile.end(), result, AZStd::regex(searchAsRegex, AZStd::regex::icase)))
{
// For each * expansion, replace the * in the destination with the expanded result
for (size_t i = 1; i < result.size(); ++i)
{
auto matchedString = result[i].str();
// Only the last match can match across directory levels
if (matchedString.find(AZ_CORRECT_FILESYSTEM_SEPARATOR) != matchedString.npos && i < result.size() - 1)
{
AZ_Error("LocalFileSCComponent", false, "Wildcard cannot match across directory levels. Please simplify your search or put a wildcard at the end of the search to match across directories.");
return {};
}
destination.replace(destination.find('*'), 1, result[i].str().c_str());
}
}
return destination;
}
void LocalFileSCComponent::RequestDeleteBulk(const char* fullFilePath, const SourceControlResponseCallbackBulk& respCallback)
{
RequestDeleteBulkExtended(fullFilePath, false, respCallback);
}
void LocalFileSCComponent::RequestDeleteBulkExtended(const char* fullFilePath, bool skipReadOnly, const SourceControlResponseCallbackBulk& respCallback)
{
AZStd::string fullFilePathString = fullFilePath; // We need a string we can copy to the job thread since fullFilePath could go out of scope before the job runs
auto job = AZ::CreateJobFunction([fullFilePathString, skipReadOnly, respCallback]() mutable
{
AZStd::vector<SourceControlFileInfo> info;
QStringList files = GetFiles(fullFilePathString.c_str());
for (QString file : files)
{
info.push_back();
auto& fileInfo = info.back();
fileInfo.m_filePath = file.toUtf8().constData();
RefreshInfoFromFileSystem(fileInfo);
if (!skipReadOnly)
{
RemoveReadOnly(fileInfo);
}
auto succeeded = AZ::IO::SystemFile::Delete(fileInfo.m_filePath.c_str());
RefreshInfoFromFileSystem(fileInfo);
fileInfo.m_status = succeeded ? SCS_OpSuccess : SCS_ProviderError;
}
AZ::TickBus::QueueFunction(respCallback, true, info);
}, true);
job->Start();
}
void LocalFileSCComponent::RequestRevert(const char* fullFilePath, const SourceControlResponseCallback& respCallback)
{
// Get the info, and fail if the file doesn't exist.
GetFileInfo(fullFilePath, respCallback);
}
void LocalFileSCComponent::RequestLatest(const char* fullFilePath, const SourceControlResponseCallback& respCallback)
{
SourceControlFileInfo fileInfo(fullFilePath);
auto job = AZ::CreateJobFunction([fileInfo, respCallback]() mutable
{
RefreshInfoFromFileSystem(fileInfo);
AZ::TickBus::QueueFunction(respCallback, true, fileInfo);
}, true);
job->Start();
}
void LocalFileSCComponent::RequestRename(const char* sourcePathFull, const char* destPathFull, const SourceControlResponseCallback& respCallback)
{
RequestRenameExtended(sourcePathFull, destPathFull, false, respCallback);
}
void LocalFileSCComponent::RequestRenameExtended(const char* sourcePathFull, const char* destPathFull, bool skipReadOnly, const SourceControlResponseCallback& respCallback)
{
SourceControlFileInfo fileInfoSrc(sourcePathFull);
SourceControlFileInfo fileInfoDst(destPathFull);
auto job = AZ::CreateJobFunction([fileInfoSrc, fileInfoDst, skipReadOnly, respCallback]() mutable
{
bool succeeded = true;
RefreshInfoFromFileSystem(fileInfoSrc);
if (!skipReadOnly || fileInfoSrc.HasFlag(SourceControlFlags::SCF_Writeable))
{
succeeded = AZ::IO::SystemFile::Rename(fileInfoSrc.m_filePath.c_str(), fileInfoDst.m_filePath.c_str());
RefreshInfoFromFileSystem(fileInfoDst);
fileInfoDst.m_status = succeeded ? SCS_OpSuccess : SCS_ProviderError;
}
else
{
fileInfoDst.m_status = SCS_ProviderError;
}
AZ::TickBus::QueueFunction(respCallback, succeeded, fileInfoDst);
}, true);
job->Start();
}
void LocalFileSCComponent::RequestRenameBulk(const char* sourcePathFull, const char* destPathFull, const SourceControlResponseCallbackBulk& respCallback)
{
RequestRenameBulkExtended(sourcePathFull, destPathFull, false, respCallback);
}
void LocalFileSCComponent::RequestRenameBulkExtended(const char* sourcePathFull, const char* destPathFull, bool skipReadOnly, const SourceControlResponseCallbackBulk& respCallback)
{
AZStd::string sourcePathFullString = sourcePathFull;
AZStd::string destPathFullString = destPathFull;
auto job = AZ::CreateJobFunction([sourcePathFullString, destPathFullString, skipReadOnly, respCallback]() mutable
{
bool success = true;
AZStd::vector<SourceControlFileInfo> info;
AZ::s64 sourceWildcardCount = std::count(sourcePathFullString.begin(), sourcePathFullString.end(), '*');
AZ::s64 destinationWildcardCount = std::count(destPathFullString.begin(), destPathFullString.end(), '*');
if (sourceWildcardCount != destinationWildcardCount)
{
success = false;
AZ_Error("LocalFileSCComponent", false, "Source and destination paths must have the same number of wildcards.");
}
else
{
QStringList files = GetFiles(sourcePathFullString.c_str());
for (QString file : files)
{
AZStd::string absFilePath(file.toUtf8().constData());
AZ::StringFunc::Replace(absFilePath, AZ_WRONG_FILESYSTEM_SEPARATOR, AZ_CORRECT_FILESYSTEM_SEPARATOR);
auto destination = ResolveWildcardDestination(absFilePath, sourcePathFullString, destPathFullString);
SourceControlFileInfo fileInfoSrc(file.toUtf8().constData());
SourceControlFileInfo fileInfoDst(destination.c_str());
RefreshInfoFromFileSystem(fileInfoSrc);
bool succeeded = false;
if (!skipReadOnly || fileInfoSrc.HasFlag(SourceControlFlags::SCF_Writeable))
{
AZStd::string destinationFolder;
AZ::StringFunc::Path::GetFullPath(fileInfoDst.m_filePath.c_str(), destinationFolder);
AZ::IO::SystemFile::CreateDir(destinationFolder.c_str());
succeeded = AZ::IO::SystemFile::Rename(file.toUtf8().constData(), fileInfoDst.m_filePath.c_str());
RefreshInfoFromFileSystem(fileInfoDst);
}
fileInfoDst.m_status = succeeded ? SCS_OpSuccess : SCS_ProviderError;
info.push_back(fileInfoDst);
}
}
AZ::TickBus::QueueFunction(respCallback, success, info);
}, true);
job->Start();
}
void LocalFileSCComponent::Reflect(AZ::ReflectContext* context)
{
AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
if (serialize)
{
serialize->Class<LocalFileSCComponent, AZ::Component>()
;
}
}
} // namespace AzToolsFramework