SettingsAccessor: Extract functionality to merge settings

Move functionality related to merging settings into MergingSettingsAccessor,
move code specific to the .user-files into UserFileAccessor.

Remove SettingsAccessor class, now that all code has been moved out of it.

This patch changes the merge mechanism a bit: It used to upgrade
the user and tha shared file to the higher version of these two, merge,
and then upgrade the merged result to the newest version.

Now it upgrades both the user as well as the shared file to the newest
version and only merges afterwards.

Change-Id: I2a1605cbe9b9fb1404fcfa9954a9f3410da0abb1
Reviewed-by: Eike Ziller <eike.ziller@qt.io>
This commit is contained in:
Tobias Hunger
2017-12-12 17:38:23 +01:00
parent 9ffd52f9c5
commit b95bbe1d57
5 changed files with 421 additions and 381 deletions

View File

@@ -38,86 +38,8 @@ namespace {
const char ORIGINAL_VERSION_KEY[] = "OriginalVersion";
const char SETTINGS_ID_KEY[] = "EnvironmentId";
const char USER_STICKY_KEYS_KEY[] = "UserStickyKeys";
const char VERSION_KEY[] = "Version";
static QString generateSuffix(const QString &suffix)
{
QString result = suffix;
result.replace(QRegExp("[^a-zA-Z0-9_.-]"), QString('_')); // replace fishy characters:
if (!result.startsWith('.'))
result.prepend('.');
return result;
}
// FIXME: Remove this!
class Operation {
public:
virtual ~Operation() { }
virtual void apply(QVariantMap &userMap, const QString &key, const QVariant &sharedValue) = 0;
void synchronize(QVariantMap &userMap, const QVariantMap &sharedMap)
{
QVariantMap::const_iterator it = sharedMap.begin();
QVariantMap::const_iterator eit = sharedMap.end();
for (; it != eit; ++it) {
const QString &key = it.key();
if (key == VERSION_KEY || key == SETTINGS_ID_KEY)
continue;
const QVariant &sharedValue = it.value();
const QVariant &userValue = userMap.value(key);
if (sharedValue.type() == QVariant::Map) {
if (userValue.type() != QVariant::Map) {
// This should happen only if the user manually changed the file in such a way.
continue;
}
QVariantMap nestedUserMap = userValue.toMap();
synchronize(nestedUserMap, sharedValue.toMap());
userMap.insert(key, nestedUserMap);
continue;
}
if (userMap.contains(key) && userValue != sharedValue) {
apply(userMap, key, sharedValue);
continue;
}
}
}
};
class MergeSettingsOperation : public Operation
{
public:
void apply(QVariantMap &userMap, const QString &key, const QVariant &sharedValue)
{
// Do not override bookkeeping settings:
if (key == ORIGINAL_VERSION_KEY || key == VERSION_KEY)
return;
if (!userMap.value(USER_STICKY_KEYS_KEY).toList().contains(key))
userMap.insert(key, sharedValue);
}
};
// When restoring settings...
// We check whether a .shared file exists. If so, we compare the settings in this file with
// corresponding ones in the .user file. Whenever we identify a corresponding setting which
// has a different value and which is not marked as sticky, we merge the .shared value into
// the .user value.
QVariantMap mergeSharedSettings(const QVariantMap &userMap, const QVariantMap &sharedMap)
{
QVariantMap result = userMap;
if (sharedMap.isEmpty())
return result;
if (userMap.isEmpty())
return sharedMap;
MergeSettingsOperation op;
op.synchronize(result, sharedMap);
return result;
}
} // namespace
namespace Utils {
@@ -314,11 +236,6 @@ BackUpStrategy::backupName(const QVariantMap &oldData, const FileName &path, con
return backup;
}
/*!
* The BackingUpSettingsAccessor extends the BasicSettingsAccessor with a way to
* keep backups. The backup strategy can be used to influence when and how backups
* are created.
*/
BackingUpSettingsAccessor::BackingUpSettingsAccessor(const QString &docType,
const QString &displayName,
const QString &applicationDisplayName) :
@@ -657,204 +574,119 @@ UpgradingSettingsAccessor::validateVersionRange(const RestoreData &data) const
}
// --------------------------------------------------------------------
// SettingsAccessorPrivate:
// MergingSettingsAccessor:
// --------------------------------------------------------------------
class SettingsAccessorPrivate
/*!
* MergingSettingsAccessor allows to merge secondary settings into the main settings.
* This is useful to e.g. handle .shared files together with .user files.
*/
MergingSettingsAccessor::MergingSettingsAccessor(std::unique_ptr<BackUpStrategy> &&strategy,
const QString &docType,
const QString &displayName,
const QString &applicationDisplayName) :
UpgradingSettingsAccessor(std::move(strategy), docType, displayName, applicationDisplayName)
{ }
BasicSettingsAccessor::RestoreData MergingSettingsAccessor::readData(const FileName &path,
QWidget *parent) const
{
public:
SettingsAccessorPrivate(const FileName &projectFilePath) : m_projectFilePath(projectFilePath) { }
const FileName m_projectFilePath;
std::unique_ptr<BasicSettingsAccessor> m_sharedFile;
};
// Return path to shared directory for .user files, create if necessary.
static inline Utils::optional<QString> defineExternalUserFileDir()
{
static const char userFilePathVariable[] = "QTC_USER_FILE_PATH";
static QString userFilePath = QFile::decodeName(qgetenv(userFilePathVariable));
if (!userFilePath.isEmpty())
return QString();
const QFileInfo fi(userFilePath);
const QString path = fi.absoluteFilePath();
if (fi.isDir() || fi.isSymLink())
return path;
if (fi.exists()) {
qWarning() << userFilePathVariable << '=' << QDir::toNativeSeparators(path)
<< " points to an existing file";
return nullopt;
RestoreData mainData = UpgradingSettingsAccessor::readData(path, parent); // FULLY upgraded!
if (mainData.hasIssue()) {
if (reportIssues(mainData.issue.value(), mainData.path, parent) == DiscardAndContinue)
mainData.data.clear();
mainData.issue = nullopt;
}
QDir dir;
if (!dir.mkpath(path)) {
qWarning() << "Cannot create: " << QDir::toNativeSeparators(path);
return nullopt;
}
return path;
}
// Return a suitable relative path to be created under the shared .user directory.
static QString makeRelative(QString path)
{
const QChar slash('/');
// Windows network shares: "//server.domain-a.com/foo' -> 'serverdomainacom/foo'
if (path.startsWith("//")) {
path.remove(0, 2);
const int nextSlash = path.indexOf(slash);
if (nextSlash > 0) {
for (int p = nextSlash; p >= 0; --p) {
if (!path.at(p).isLetterOrNumber())
path.remove(p, 1);
}
}
return path;
}
// Windows drives: "C:/foo' -> 'c/foo'
if (path.size() > 3 && path.at(1) == ':') {
path.remove(1, 1);
path[0] = path.at(0).toLower();
return path;
}
if (path.startsWith(slash)) // Standard UNIX paths: '/foo' -> 'foo'
path.remove(0, 1);
return path;
}
// Return complete file path of the .user file.
static FileName externalUserFilePath(const Utils::FileName &projectFilePath, const QString &suffix)
{
FileName result;
static const optional<QString> externalUserFileDir = defineExternalUserFileDir();
if (!externalUserFileDir) {
// Recreate the relative project file hierarchy under the shared directory.
// PersistentSettingsWriter::write() takes care of creating the path.
result = FileName::fromString(externalUserFileDir.value());
result.appendString('/' + makeRelative(projectFilePath.toString()));
result.appendString(suffix);
}
return result;
}
// -----------------------------------------------------------------------------
// SettingsAccessor:
// -----------------------------------------------------------------------------
SettingsAccessor::SettingsAccessor(std::unique_ptr<BackUpStrategy> &&strategy,
const Utils::FileName &baseFile, const QString &docType,
const QString &displayName, const QString &appDisplayName) :
UpgradingSettingsAccessor(std::move(strategy), docType, displayName, appDisplayName),
d(new SettingsAccessorPrivate(baseFile))
{
const FileName externalUser = externalUserFile();
const FileName projectUser = projectUserFile();
setBaseFilePath(externalUser.isEmpty() ? projectUser : externalUser);
d->m_sharedFile
= std::make_unique<BasicSettingsAccessor>(docType, displayName, appDisplayName);
d->m_sharedFile->setBaseFilePath(sharedFile());
}
SettingsAccessor::~SettingsAccessor()
{
delete d;
}
void SettingsAccessor::storeSharedSettings(const QVariantMap &data) const
{
Q_UNUSED(data);
}
FileName SettingsAccessor::projectUserFile() const
{
static const QString qtcExt = QLatin1String(qgetenv("QTC_EXTENSION"));
FileName projectUserFile = d->m_projectFilePath;
projectUserFile.appendString(generateSuffix(qtcExt.isEmpty() ? ".user" : qtcExt));
return projectUserFile;
}
FileName SettingsAccessor::externalUserFile() const
{
static const QString qtcExt = QLatin1String(qgetenv("QTC_EXTENSION"));
return externalUserFilePath(d->m_projectFilePath, generateSuffix(qtcExt.isEmpty() ? ".user" : qtcExt));
}
FileName SettingsAccessor::sharedFile() const
{
static const QString qtcExt = QLatin1String(qgetenv("QTC_SHARED_EXTENSION"));
FileName sharedFile = d->m_projectFilePath;
sharedFile.appendString(generateSuffix(qtcExt.isEmpty() ? ".shared" : qtcExt));
return sharedFile;
}
BasicSettingsAccessor::RestoreData SettingsAccessor::readData(const FileName &path,
QWidget *parent) const
{
Q_UNUSED(path); // FIXME: This is wrong!
RestoreData userSettings = UpgradingSettingsAccessor::readData(path, parent); // FULLY updated!
if (userSettings.hasIssue() && reportIssues(userSettings.issue.value(), userSettings.path, parent) == DiscardAndContinue)
userSettings.data.clear();
RestoreData sharedSettings = readSharedSettings(parent);
if (sharedSettings.hasIssue() && reportIssues(sharedSettings.issue.value(), sharedSettings.path, parent) == DiscardAndContinue)
sharedSettings.data.clear();
RestoreData mergedSettings = RestoreData(userSettings.path,
mergeSettings(userSettings.data, sharedSettings.data));
return mergedSettings;
}
SettingsAccessor::RestoreData SettingsAccessor::readSharedSettings(QWidget *parent) const
{
RestoreData sharedSettings = d->m_sharedFile->readData(d->m_sharedFile->baseFilePath(), parent);
if (versionFromMap(sharedSettings.data) > currentVersion()) {
// The shared file version is newer than Creator... If we have valid user
RestoreData secondaryData
= m_secondaryAccessor ? m_secondaryAccessor->readData(m_secondaryAccessor->baseFilePath(), parent)
: RestoreData();
int secondaryVersion = versionFromMap(secondaryData.data);
if (secondaryVersion == -1)
secondaryVersion = currentVersion(); // No version information, use currentVersion since
// trying to upgrade makes no sense without an idea
// of what might have changed in the meantime.b
if (!secondaryData.hasIssue() && !secondaryData.data.isEmpty()
&& (secondaryVersion < firstSupportedVersion() || secondaryVersion > currentVersion())) {
// The shared file version is too old/too new for Creator... If we have valid user
// settings we prompt the user whether we could try an *unsupported* update.
// This makes sense since the merging operation will only replace shared settings
// that perfectly match corresponding user ones. If we don't have valid user
// settings to compare against, there's nothing we can do.
sharedSettings.issue = Issue(QApplication::translate("Utils::SettingsAccessor",
"Unsupported Shared Settings File"),
QApplication::translate("Utils::SettingsAccessor",
"The version of your .shared file is not "
"supported by %1. "
"Do you want to try loading it anyway?"),
Issue::Type::WARNING);
sharedSettings.issue->buttons.insert(QMessageBox::Yes, Continue);
sharedSettings.issue->buttons.insert(QMessageBox::No, DiscardAndContinue);
sharedSettings.issue->defaultButton = QMessageBox::No;
sharedSettings.issue->escapeButton = QMessageBox::No;
secondaryData.issue = Issue(QApplication::translate("Utils::BasicSettingsAccessor",
"Unsupported Merge Settings File"),
QApplication::translate("Utils::BasicSettingsAccessor",
"\"%1\" is not supported by %1. "
"Do you want to try loading it anyway?")
.arg(secondaryData.path.toUserOutput())
.arg(applicationDisplayName), Issue::Type::WARNING);
secondaryData.issue->buttons.insert(QMessageBox::Yes, Continue);
secondaryData.issue->buttons.insert(QMessageBox::No, DiscardAndContinue);
secondaryData.issue->defaultButton = QMessageBox::No;
secondaryData.issue->escapeButton = QMessageBox::No;
}
return sharedSettings;
if (secondaryData.hasIssue()) {
if (reportIssues(secondaryData.issue.value(), secondaryData.path, parent) == DiscardAndContinue)
secondaryData.data.clear();
secondaryData.issue = nullopt;
}
if (!secondaryData.data.isEmpty())
secondaryData = upgradeSettings(secondaryData, currentVersion());
return mergeSettings(mainData, secondaryData);
}
QVariantMap
SettingsAccessor::mergeSettings(const QVariantMap &userMap, const QVariantMap &sharedMap) const
void MergingSettingsAccessor::setSecondaryAccessor(std::unique_ptr<BasicSettingsAccessor> &&secondary)
{
QVariantMap newUser = userMap;
QVariantMap newShared = sharedMap;
m_secondaryAccessor = std::move(secondary);
}
const int userVersion = versionFromMap(userMap);
const int sharedVersion = versionFromMap(sharedMap);
/*!
* Merge \a secondary into \a main. Both need to be at the newest possible version.
*/
BasicSettingsAccessor::RestoreData
MergingSettingsAccessor::mergeSettings(const BasicSettingsAccessor::RestoreData &main,
const BasicSettingsAccessor::RestoreData &secondary) const
{
const int mainVersion = versionFromMap(main.data);
const int secondaryVersion = versionFromMap(secondary.data);
QVariantMap result;
if (!newUser.isEmpty() && !newShared.isEmpty()) {
newUser = upgradeSettings(RestoreData(Utils::FileName::fromLatin1("main"), newUser), sharedVersion).data;
newShared = upgradeSettings(RestoreData(Utils::FileName::fromLatin1("secondary"), newShared), userVersion).data;
result = mergeSharedSettings(newUser, newShared);
} else if (!sharedMap.isEmpty()) {
result = sharedMap;
} else if (!userMap.isEmpty()) {
result = userMap;
}
QTC_CHECK(main.data.isEmpty() || mainVersion == currentVersion());
QTC_CHECK(secondary.data.isEmpty() || secondaryVersion == currentVersion());
storeSharedSettings(newShared);
if (main.data.isEmpty())
return secondary;
else if (secondary.data.isEmpty())
return main;
SettingsMergeFunction mergeFunction
= [this](const SettingsMergeData &global, const SettingsMergeData &local) {
return merge(global, local);
};
const QVariantMap result = mergeQVariantMaps(main.data, secondary.data, mergeFunction).toMap();
// Update from the base version to Creator's version.
return upgradeSettings(RestoreData(Utils::FileName::fromLatin1("result"), result), currentVersion()).data;
return RestoreData(main.path, postprocessMerge(main.data, secondary.data, result));
}
/*!
* Returns true for housekeeping related keys.
*/
bool MergingSettingsAccessor::isHouseKeepingKey(const QString &key) const
{
return key == VERSION_KEY || key == ORIGINAL_VERSION_KEY || key == SETTINGS_ID_KEY;
}
QVariantMap MergingSettingsAccessor::postprocessMerge(const QVariantMap &main,
const QVariantMap &secondary,
const QVariantMap &result) const
{
Q_UNUSED(main);
Q_UNUSED(secondary);
return result;
}
// --------------------------------------------------------------------
@@ -891,4 +723,45 @@ void setSettingsIdInMap(QVariantMap &data, const QByteArray &id)
data.insert(SETTINGS_ID_KEY, id);
}
static QVariant mergeQVariantMapsRecursion(const QVariantMap &mainTree, const QVariantMap &secondaryTree,
const QString &keyPrefix,
const QVariantMap &mainSubtree, const QVariantMap &secondarySubtree,
const SettingsMergeFunction &merge)
{
QVariantMap result;
const QList<QString> allKeys = Utils::filteredUnique(mainSubtree.keys() + secondarySubtree.keys());
MergingSettingsAccessor::SettingsMergeData global = {mainTree, secondaryTree, QString()};
MergingSettingsAccessor::SettingsMergeData local = {mainSubtree, secondarySubtree, QString()};
for (const QString &key : allKeys) {
global.key = keyPrefix + key;
local.key = key;
Utils::optional<QPair<QString, QVariant>> mergeResult = merge(global, local);
if (!mergeResult)
continue;
QPair<QString, QVariant> kv = mergeResult.value();
if (kv.second.type() == QVariant::Map) {
const QString newKeyPrefix = keyPrefix + kv.first + '/';
kv.second = mergeQVariantMapsRecursion(mainTree, secondaryTree, newKeyPrefix,
kv.second.toMap(), secondarySubtree.value(kv.first)
.toMap(), merge);
}
if (!kv.second.isNull())
result.insert(kv.first, kv.second);
}
return result;
}
QVariant mergeQVariantMaps(const QVariantMap &mainTree, const QVariantMap &secondaryTree,
const SettingsMergeFunction &merge)
{
return mergeQVariantMapsRecursion(mainTree, secondaryTree, QString(),
mainTree, secondaryTree, merge);
}
} // namespace Utils

View File

@@ -51,6 +51,20 @@ QTCREATOR_UTILS_EXPORT void setVersionInMap(QVariantMap &data, int version);
QTCREATOR_UTILS_EXPORT void setOriginalVersionInMap(QVariantMap &data, int version);
QTCREATOR_UTILS_EXPORT void setSettingsIdInMap(QVariantMap &data, const QByteArray &id);
// --------------------------------------------------------------------
// Helpers:
// --------------------------------------------------------------------
QTCREATOR_UTILS_EXPORT int versionFromMap(const QVariantMap &data);
QTCREATOR_UTILS_EXPORT int originalVersionFromMap(const QVariantMap &data);
QTCREATOR_UTILS_EXPORT QByteArray settingsIdFromMap(const QVariantMap &data);
QTCREATOR_UTILS_EXPORT void setVersionInMap(QVariantMap &data, int version);
QTCREATOR_UTILS_EXPORT void setOriginalVersionInMap(QVariantMap &data, int version);
QTCREATOR_UTILS_EXPORT void setSettingsIdInMap(QVariantMap &data, const QByteArray &id);
using SettingsMergeResult = Utils::optional<QPair<QString, QVariant>>;
// --------------------------------------------------------------------
// BasicSettingsAccessor:
// --------------------------------------------------------------------
@@ -215,7 +229,7 @@ private:
const QString m_extension;
};
class SettingsAccessor;
class MergingSettingsAccessor;
class QTCREATOR_UTILS_EXPORT UpgradingSettingsAccessor : public BackingUpSettingsAccessor
{
@@ -233,9 +247,9 @@ public:
bool isValidVersionAndId(const int version, const QByteArray &id) const;
VersionUpgrader *upgrader(const int version) const;
protected:
RestoreData readData(const Utils::FileName &path, QWidget *parent) const override;
protected:
QVariantMap prepareToWriteSettings(const QVariantMap &data) const override;
void setSettingsId(const QByteArray &id) { m_id = id; }
@@ -250,36 +264,44 @@ private:
};
// --------------------------------------------------------------------
// SettingsAccessor:
// MergingSettingsAccessor:
// --------------------------------------------------------------------
class SettingsAccessorPrivate;
class QTCREATOR_UTILS_EXPORT SettingsAccessor : public UpgradingSettingsAccessor
class QTCREATOR_UTILS_EXPORT MergingSettingsAccessor : public UpgradingSettingsAccessor
{
public:
explicit SettingsAccessor(std::unique_ptr<BackUpStrategy> &&strategy,
const Utils::FileName &baseFile, const QString &docType,
const QString &displayName, const QString &applicationDisplayName);
~SettingsAccessor() override;
struct SettingsMergeData {
QVariantMap main;
QVariantMap secondary;
QString key;
};
Utils::FileName projectUserFile() const;
Utils::FileName externalUserFile() const;
Utils::FileName sharedFile() const;
MergingSettingsAccessor(std::unique_ptr<BackUpStrategy> &&strategy,
const QString &docType, const QString &displayName,
const QString &applicationDisplayName);
protected:
RestoreData readData(const Utils::FileName &path, QWidget *parent) const final;
virtual void storeSharedSettings(const QVariantMap &data) const;
void setSecondaryAccessor(std::unique_ptr<BasicSettingsAccessor> &&secondary);
QVariantMap mergeSettings(const QVariantMap &userMap, const QVariantMap &sharedMap) const;
protected:
RestoreData mergeSettings(const RestoreData &main, const RestoreData &secondary) const;
virtual SettingsMergeResult merge(const SettingsMergeData &global,
const SettingsMergeData &local) const = 0;
bool isHouseKeepingKey(const QString &key) const;
virtual QVariantMap postprocessMerge(const QVariantMap &main, const QVariantMap &secondary,
const QVariantMap &result) const;
private:
RestoreData readSharedSettings(QWidget *parent) const;
SettingsAccessorPrivate *d;
friend class SettingsAccessorPrivate;
std::unique_ptr<BasicSettingsAccessor> m_secondaryAccessor;
};
using SettingsMergeFunction = std::function<SettingsMergeResult(const MergingSettingsAccessor::SettingsMergeData &,
const MergingSettingsAccessor::SettingsMergeData &)>;
QTCREATOR_UTILS_EXPORT QVariant mergeQVariantMaps(const QVariantMap &mainTree, const QVariantMap &secondaryTree,
const SettingsMergeFunction &merge);
} // namespace Utils

View File

@@ -48,8 +48,6 @@ using namespace ProjectExplorer::Internal;
namespace {
const char SETTINGS_ID_KEY[] = "EnvironmentId";
const char VERSION_KEY[] = "Version";
const char OBSOLETE_VERSION_KEY[] = "ProjectExplorer.Project.Updater.FileVersion";
const char SHARED_SETTINGS[] = "SharedSettings";
const char USER_STICKY_KEYS_KEY[] = "UserStickyKeys";
@@ -331,68 +329,80 @@ static QVariantMap processHandlerNodes(const HandlerNode &node, const QVariantMa
namespace {
class Operation {
public:
virtual ~Operation() { }
static QString generateSuffix(const QString &suffix)
{
QString result = suffix;
result.replace(QRegExp("[^a-zA-Z0-9_.-]"), QString('_')); // replace fishy character
if (!result.startsWith('.'))
result.prepend('.');
return result;
}
virtual void apply(QVariantMap &userMap, const QString &key, const QVariant &sharedValue) = 0;
// Return path to shared directory for .user files, create if necessary.
static inline Utils::optional<QString> defineExternalUserFileDir()
{
static const char userFilePathVariable[] = "QTC_USER_FILE_PATH";
static QString userFilePath = QFile::decodeName(qgetenv(userFilePathVariable));
if (userFilePath.isEmpty())
return QString();
const QFileInfo fi(userFilePath);
const QString path = fi.absoluteFilePath();
if (fi.isDir() || fi.isSymLink())
return path;
if (fi.exists()) {
qWarning() << userFilePathVariable << '=' << QDir::toNativeSeparators(path)
<< " points to an existing file";
return nullopt;
}
QDir dir;
if (!dir.mkpath(path)) {
qWarning() << "Cannot create: " << QDir::toNativeSeparators(path);
return nullopt;
}
return path;
}
void synchronize(QVariantMap &userMap, const QVariantMap &sharedMap)
{
QVariantMap::const_iterator it = sharedMap.begin();
QVariantMap::const_iterator eit = sharedMap.end();
for (; it != eit; ++it) {
const QString &key = it.key();
if (key == VERSION_KEY || key == SETTINGS_ID_KEY)
continue;
const QVariant &sharedValue = it.value();
const QVariant &userValue = userMap.value(key);
if (sharedValue.type() == QVariant::Map) {
if (userValue.type() != QVariant::Map) {
// This should happen only if the user manually changed the file in such a way.
continue;
}
QVariantMap nestedUserMap = userValue.toMap();
synchronize(nestedUserMap, sharedValue.toMap());
userMap.insert(key, nestedUserMap);
continue;
}
if (userMap.contains(key) && userValue != sharedValue) {
apply(userMap, key, sharedValue);
continue;
// Return a suitable relative path to be created under the shared .user directory.
static QString makeRelative(QString path)
{
const QChar slash('/');
// Windows network shares: "//server.domain-a.com/foo' -> 'serverdomainacom/foo'
if (path.startsWith("//")) {
path.remove(0, 2);
const int nextSlash = path.indexOf(slash);
if (nextSlash > 0) {
for (int p = nextSlash; p >= 0; --p) {
if (!path.at(p).isLetterOrNumber())
path.remove(p, 1);
}
}
return path;
}
};
class TrackStickyness : public Operation
{
public:
void apply(QVariantMap &userMap, const QString &key, const QVariant &)
{
const QString stickyKey = USER_STICKY_KEYS_KEY;
QVariantList sticky = userMap.value(stickyKey).toList();
sticky.append(key);
userMap.insert(stickyKey, sticky);
// Windows drives: "C:/foo' -> 'c/foo'
if (path.size() > 3 && path.at(1) == ':') {
path.remove(1, 1);
path[0] = path.at(0).toLower();
return path;
}
};
if (path.startsWith(slash)) // Standard UNIX paths: '/foo' -> 'foo'
path.remove(0, 1);
return path;
}
// When saving settings...
// If a .shared file was considered in the previous restoring step, we check whether for
// any of the current .shared settings there's a .user one which is different. If so, this
// means the user explicitly changed it and we mark this setting as sticky.
// Note that settings are considered sticky only when they differ from the .shared ones.
// Although this approach is more flexible than permanent/forever sticky settings, it has
// the side-effect that if a particular value unintentionally becomes the same in both
// the .user and .shared files, this setting will "unstick".
void trackUserStickySettings(QVariantMap &userMap, const QVariantMap &sharedMap)
// Return complete file path of the .user file.
static FileName externalUserFilePath(const Utils::FileName &projectFilePath, const QString &suffix)
{
if (sharedMap.isEmpty())
return;
FileName result;
static const optional<QString> externalUserFileDir = defineExternalUserFileDir();
TrackStickyness op;
op.synchronize(userMap, sharedMap);
if (!externalUserFileDir) {
// Recreate the relative project file hierarchy under the shared directory.
// PersistentSettingsWriter::write() takes care of creating the path.
result = FileName::fromString(externalUserFileDir.value());
result.appendString('/' + makeRelative(projectFilePath.toString()));
result.appendString(suffix);
}
return result;
}
} // namespace
@@ -430,11 +440,21 @@ FileNameList UserFileBackUpStrategy::readFileCandidates(const FileName &baseFile
// --------------------------------------------------------------------
UserFileAccessor::UserFileAccessor(Project *project) :
SettingsAccessor(std::make_unique<UserFileBackUpStrategy>(this),
project->projectFilePath(), "QtCreatorProject",
project->displayName(), Core::Constants::IDE_DISPLAY_NAME),
MergingSettingsAccessor(std::make_unique<VersionedBackUpStrategy>(this),
"QtCreatorProject", project->displayName(),
Core::Constants::IDE_DISPLAY_NAME),
m_project(project)
{
// Setup:
const FileName externalUser = externalUserFile();
const FileName projectUser = projectUserFile();
setBaseFilePath(externalUser.isEmpty() ? projectUser : externalUser);
auto secondary
= std::make_unique<BasicSettingsAccessor>(docType, displayName, applicationDisplayName);
secondary->setBaseFilePath(sharedFile());
setSecondaryAccessor(std::move(secondary));
setSettingsId(ProjectExplorerPlugin::projectExplorerSettings().environmentId.toByteArray());
// Register Upgraders:
@@ -462,9 +482,61 @@ Project *UserFileAccessor::project() const
return m_project;
}
void UserFileAccessor::storeSharedSettings(const QVariantMap &data) const
SettingsMergeResult
UserFileAccessor::merge(const MergingSettingsAccessor::SettingsMergeData &global,
const MergingSettingsAccessor::SettingsMergeData &local) const
{
project()->setProperty(SHARED_SETTINGS, data);
const QStringList stickyKeys = global.main.value(USER_STICKY_KEYS_KEY).toStringList();
const QString key = local.key;
const QVariant mainValue = local.main.value(key);
const QVariant secondaryValue = local.secondary.value(key);
if (mainValue.isNull() && secondaryValue.isNull())
return nullopt;
if (isHouseKeepingKey(key) || global.key == USER_STICKY_KEYS_KEY)
return qMakePair(key, mainValue);
if (!stickyKeys.contains(global.key) && secondaryValue != mainValue && !secondaryValue.isNull())
return qMakePair(key, secondaryValue);
if (!mainValue.isNull())
return qMakePair(key, mainValue);
return qMakePair(key, secondaryValue);
}
// When saving settings...
// If a .shared file was considered in the previous restoring step, we check whether for
// any of the current .shared settings there's a .user one which is different. If so, this
// means the user explicitly changed it and we mark this setting as sticky.
// Note that settings are considered sticky only when they differ from the .shared ones.
// Although this approach is more flexible than permanent/forever sticky settings, it has
// the side-effect that if a particular value unintentionally becomes the same in both
// the .user and .shared files, this setting will "unstick".
SettingsMergeFunction UserFileAccessor::userStickyTrackerFunction(QStringList &stickyKeys) const
{
return [this, &stickyKeys](const SettingsMergeData &global, const SettingsMergeData &local)
-> SettingsMergeResult {
const QString key = local.key;
const QVariant main = local.main.value(key);
const QVariant secondary = local.secondary.value(key);
if (main.isNull()) // skip stuff not in main!
return nullopt;
if (isHouseKeepingKey(key))
return qMakePair(key, main);
// Ignore house keeping keys:
if (key == USER_STICKY_KEYS_KEY)
return nullopt;
// Track keys that changed in main from the value in secondary:
if (main != secondary && !secondary.isNull() && !stickyKeys.contains(global.key))
stickyKeys.append(global.key);
return qMakePair(key, main);
};
}
QVariant UserFileAccessor::retrieveSharedSettings() const
@@ -472,9 +544,40 @@ QVariant UserFileAccessor::retrieveSharedSettings() const
return project()->property(SHARED_SETTINGS);
}
FileName UserFileAccessor::projectUserFile() const
{
static const QString qtcExt = QLatin1String(qgetenv("QTC_SHARED_EXTENSION"));
FileName projectUserFile = m_project->projectFilePath();
projectUserFile.appendString(generateSuffix(qtcExt.isEmpty() ? ".user" : qtcExt));
return projectUserFile;
}
FileName UserFileAccessor::externalUserFile() const
{
static const QString qtcExt = QFile::decodeName(qgetenv("QTC_EXTENSION"));
return externalUserFilePath(m_project->projectFilePath(),
generateSuffix(qtcExt.isEmpty() ? ".user" : qtcExt));
}
FileName UserFileAccessor::sharedFile() const
{
static const QString qtcExt = QLatin1String(qgetenv("QTC_SHARED_EXTENSION"));
FileName sharedFile = m_project->projectFilePath();
sharedFile.appendString(generateSuffix(qtcExt.isEmpty() ? ".shared" : qtcExt));
return sharedFile;
}
QVariantMap UserFileAccessor::postprocessMerge(const QVariantMap &main,
const QVariantMap &secondary,
const QVariantMap &result) const
{
project()->setProperty(SHARED_SETTINGS, secondary);
return MergingSettingsAccessor::postprocessMerge(main, secondary, result);
}
QVariantMap UserFileAccessor::preprocessReadSettings(const QVariantMap &data) const
{
QVariantMap tmp = SettingsAccessor::preprocessReadSettings(data);
QVariantMap tmp = MergingSettingsAccessor::preprocessReadSettings(data);
// Move from old Version field to new one:
// This can not be done in a normal upgrader since the version information is needed
@@ -491,11 +594,17 @@ QVariantMap UserFileAccessor::preprocessReadSettings(const QVariantMap &data) co
QVariantMap UserFileAccessor::prepareToWriteSettings(const QVariantMap &data) const
{
QVariantMap result = SettingsAccessor::prepareToWriteSettings(data);
const QVariant shared = retrieveSharedSettings();
if (shared.isValid())
trackUserStickySettings(result, shared.toMap());
const QVariantMap tmp = MergingSettingsAccessor::prepareToWriteSettings(data);
const QVariantMap shared = retrieveSharedSettings().toMap();
QVariantMap result;
if (!shared.isEmpty()) {
QStringList stickyKeys;
SettingsMergeFunction merge = userStickyTrackerFunction(stickyKeys);
result = mergeQVariantMaps(tmp, shared, merge).toMap();
result.insert(USER_STICKY_KEYS_KEY, stickyKeys);
} else {
result = tmp;
}
// for compatibility with QtC 3.1 and older:
result.insert(OBSOLETE_VERSION_KEY, currentVersion());
@@ -2091,7 +2200,7 @@ class TestUserFileAccessor : public UserFileAccessor
public:
TestUserFileAccessor(Project *project) : UserFileAccessor(project) { }
void storeSharedSettings(const QVariantMap &data) const final { m_storedSettings = data; }
void storeSharedSettings(const QVariantMap &data) const { m_storedSettings = data; }
QVariant retrieveSharedSettings() const { return m_storedSettings; }
using UserFileAccessor::preprocessReadSettings;
@@ -2186,6 +2295,7 @@ void ProjectExplorerPlugin::testUserFileAccessor_prepareToWriteSettings()
projectExplorerSettings().environmentId.toByteArray());
QCOMPARE(result.value("UserStickyKeys"), QVariant(QStringList({"shared1"})));
QCOMPARE(result.value("Version").toInt(), accessor.currentVersion());
QCOMPARE(result.value("ProjectExplorer.Project.Updater.FileVersion"), accessor.currentVersion());
QCOMPARE(result.value("shared1"), data.value("shared1"));
QCOMPARE(result.value("shared3"), data.value("shared3"));
QCOMPARE(result.value("unique1"), data.value("unique1"));
@@ -2198,10 +2308,10 @@ void ProjectExplorerPlugin::testUserFileAccessor_mergeSettings()
QVariantMap sharedData;
sharedData.insert("Version", accessor.currentVersion());
sharedData.insert("EnvironmentId", "foobar");
sharedData.insert("shared1", "bar");
sharedData.insert("shared2", "baz");
sharedData.insert("shared3", "foooo");
TestUserFileAccessor::RestoreData shared(FileName::fromString("/shared/data"), sharedData);
QVariantMap data;
data.insert("Version", accessor.currentVersion());
@@ -2210,19 +2320,20 @@ void ProjectExplorerPlugin::testUserFileAccessor_mergeSettings()
data.insert("shared1", "bar1");
data.insert("unique1", 1234);
data.insert("shared3", "foo");
QVariantMap result = accessor.mergeSettings(data, sharedData);
TestUserFileAccessor::RestoreData user(FileName::fromString("/user/data"), data);
TestUserFileAccessor::RestoreData result = accessor.mergeSettings(user, shared);
QCOMPARE(result.count(), data.count() + 1);
QCOMPARE(result.value("OriginalVersion").toInt(), accessor.currentVersion());
QCOMPARE(result.value("EnvironmentId").toByteArray(),
QVERIFY(!result.hasIssue());
QCOMPARE(result.data.count(), data.count() + 1);
// mergeSettings does not run updateSettings, so no OriginalVersion will be set
QCOMPARE(result.data.value("EnvironmentId").toByteArray(),
projectExplorerSettings().environmentId.toByteArray()); // unchanged
QCOMPARE(result.value("UserStickyKeys"), QVariant(QStringList({"shared1"}))); // unchanged
QCOMPARE(result.value("Version").toInt(), accessor.currentVersion()); // forced
QCOMPARE(result.value("shared1"), data.value("shared1")); // from data
// FIXME: Why is this missing?
// QCOMPARE(result.value("shared2"), sharedData.value("shared2")); // from shared, missing!
QCOMPARE(result.value("shared3"), sharedData.value("shared3")); // from shared
QCOMPARE(result.value("unique1"), data.value("unique1"));
QCOMPARE(result.data.value("UserStickyKeys"), QVariant(QStringList({"shared1"}))); // unchanged
QCOMPARE(result.data.value("Version").toInt(), accessor.currentVersion()); // forced
QCOMPARE(result.data.value("shared1"), data.value("shared1")); // from data
QCOMPARE(result.data.value("shared2"), sharedData.value("shared2")); // from shared, missing!
QCOMPARE(result.data.value("shared3"), sharedData.value("shared3")); // from shared
QCOMPARE(result.data.value("unique1"), data.value("unique1"));
}
void ProjectExplorerPlugin::testUserFileAccessor_mergeSettingsEmptyUser()
@@ -2232,17 +2343,18 @@ void ProjectExplorerPlugin::testUserFileAccessor_mergeSettingsEmptyUser()
QVariantMap sharedData;
sharedData.insert("Version", accessor.currentVersion());
sharedData.insert("EnvironmentId", "foobar");
sharedData.insert("shared1", "bar");
sharedData.insert("shared2", "baz");
sharedData.insert("shared3", "foooo");
TestUserFileAccessor::RestoreData shared(FileName::fromString("/shared/data"), sharedData);
QVariantMap data;
QVariantMap result = accessor.mergeSettings(data, sharedData);
TestUserFileAccessor::RestoreData user(FileName::fromString("/shared/data"), data);
QCOMPARE(result.value("OriginalVersion").toInt(), accessor.currentVersion());
result.remove("OriginalVersion");
QCOMPARE(result, sharedData);
TestUserFileAccessor::RestoreData result = accessor.mergeSettings(user, shared);
QVERIFY(!result.hasIssue());
QCOMPARE(result.data, sharedData);
}
void ProjectExplorerPlugin::testUserFileAccessor_mergeSettingsEmptyShared()
@@ -2251,19 +2363,22 @@ void ProjectExplorerPlugin::testUserFileAccessor_mergeSettingsEmptyShared()
TestUserFileAccessor accessor(&project);
QVariantMap sharedData;
TestUserFileAccessor::RestoreData shared(FileName::fromString("/shared/data"), sharedData);
QVariantMap data;
data.insert("Version", accessor.currentVersion());
data.insert("OriginalVersion", accessor.currentVersion());
data.insert("EnvironmentId", projectExplorerSettings().environmentId.toByteArray());
data.insert("UserStickyKeys", QStringList({"shared1"}));
data.insert("shared1", "bar1");
data.insert("unique1", 1234);
data.insert("shared3", "foo");
QVariantMap result = accessor.mergeSettings(data, sharedData);
TestUserFileAccessor::RestoreData user(FileName::fromString("/shared/data"), data);
QCOMPARE(result.value("OriginalVersion").toInt(), accessor.currentVersion());
result.remove("OriginalVersion");
QCOMPARE(result, data);
TestUserFileAccessor::RestoreData result = accessor.mergeSettings(user, shared);
QVERIFY(!result.hasIssue());
QCOMPARE(result.data, data);
}
#endif // WITH_TESTS

View File

@@ -37,7 +37,8 @@ namespace ProjectExplorer {
class Project;
namespace Internal {
class UserFileAccessor : public Utils::SettingsAccessor
class UserFileAccessor : public Utils::MergingSettingsAccessor
{
public:
UserFileAccessor(Project *project);
@@ -46,13 +47,23 @@ public:
virtual QVariant retrieveSharedSettings() const;
Utils::FileName projectUserFile() const;
Utils::FileName externalUserFile() const;
Utils::FileName sharedFile() const;
protected:
QVariantMap postprocessMerge(const QVariantMap &main,
const QVariantMap &secondary,
const QVariantMap &result) const final;
QVariantMap preprocessReadSettings(const QVariantMap &data) const final;
QVariantMap prepareToWriteSettings(const QVariantMap &data) const final;
void storeSharedSettings(const QVariantMap &data) const override;
Utils::SettingsMergeResult merge(const SettingsMergeData &global,
const SettingsMergeData &local) const final;
private:
Utils::SettingsMergeFunction userStickyTrackerFunction(QStringList &stickyKeys) const;
Project *m_project;
};