summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTobias Hunger <tobias.hunger@qt.io>2017-12-12 17:38:23 +0100
committerTobias Hunger <tobias.hunger@qt.io>2018-02-14 13:23:33 +0000
commitb95bbe1d57d5c55d714fae0a094b149faa02432c (patch)
tree280ac9048ddb4035df4c24b29a93237de223f746
parent9ffd52f9c54a95ad280ee70275ed1e06272add10 (diff)
downloadqt-creator-b95bbe1d57d5c55d714fae0a094b149faa02432c.tar.gz
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>
-rw-r--r--src/libs/utils/settingsaccessor.cpp381
-rw-r--r--src/libs/utils/settingsaccessor.h62
-rw-r--r--src/plugins/projectexplorer/userfileaccessor.cpp291
-rw-r--r--src/plugins/projectexplorer/userfileaccessor.h17
-rw-r--r--tests/auto/utils/settings/tst_settings.cpp33
5 files changed, 412 insertions, 372 deletions
diff --git a/src/libs/utils/settingsaccessor.cpp b/src/libs/utils/settingsaccessor.cpp
index 613c0bde2e..766f2eac9d 100644
--- a/src/libs/utils/settingsaccessor.cpp
+++ b/src/libs/utils/settingsaccessor.cpp
@@ -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
-{
-public:
- SettingsAccessorPrivate(const FileName &projectFilePath) : m_projectFilePath(projectFilePath) { }
-
- const FileName m_projectFilePath;
-
- std::unique_ptr<BasicSettingsAccessor> m_sharedFile;
-};
+/*!
+ * 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)
+{ }
-// Return path to shared directory for .user files, create if necessary.
-static inline Utils::optional<QString> defineExternalUserFileDir()
+BasicSettingsAccessor::RestoreData MergingSettingsAccessor::readData(const FileName &path,
+ QWidget *parent) const
{
- 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;
-}
-
-// 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;
+ 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;
}
- // 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();
+ 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.
- 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);
+ 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 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());
-}
+ if (secondaryData.hasIssue()) {
+ if (reportIssues(secondaryData.issue.value(), secondaryData.path, parent) == DiscardAndContinue)
+ secondaryData.data.clear();
+ secondaryData.issue = nullopt;
+ }
-SettingsAccessor::~SettingsAccessor()
-{
- delete d;
-}
+ if (!secondaryData.data.isEmpty())
+ secondaryData = upgradeSettings(secondaryData, currentVersion());
-void SettingsAccessor::storeSharedSettings(const QVariantMap &data) const
-{
- Q_UNUSED(data);
+ return mergeSettings(mainData, secondaryData);
}
-FileName SettingsAccessor::projectUserFile() const
+void MergingSettingsAccessor::setSecondaryAccessor(std::unique_ptr<BasicSettingsAccessor> &&secondary)
{
- static const QString qtcExt = QLatin1String(qgetenv("QTC_EXTENSION"));
- FileName projectUserFile = d->m_projectFilePath;
- projectUserFile.appendString(generateSuffix(qtcExt.isEmpty() ? ".user" : qtcExt));
- return projectUserFile;
+ m_secondaryAccessor = std::move(secondary);
}
-FileName SettingsAccessor::externalUserFile() const
+/*!
+ * 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
{
- static const QString qtcExt = QLatin1String(qgetenv("QTC_EXTENSION"));
- return externalUserFilePath(d->m_projectFilePath, generateSuffix(qtcExt.isEmpty() ? ".user" : qtcExt));
-}
+ const int mainVersion = versionFromMap(main.data);
+ const int secondaryVersion = versionFromMap(secondary.data);
-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;
-}
+ QTC_CHECK(main.data.isEmpty() || mainVersion == currentVersion());
+ QTC_CHECK(secondary.data.isEmpty() || secondaryVersion == currentVersion());
-BasicSettingsAccessor::RestoreData SettingsAccessor::readData(const FileName &path,
- QWidget *parent) const
-{
- Q_UNUSED(path); // FIXME: This is wrong!
+ if (main.data.isEmpty())
+ return secondary;
+ else if (secondary.data.isEmpty())
+ return main;
- RestoreData userSettings = UpgradingSettingsAccessor::readData(path, parent); // FULLY updated!
- if (userSettings.hasIssue() && reportIssues(userSettings.issue.value(), userSettings.path, parent) == DiscardAndContinue)
- userSettings.data.clear();
+ SettingsMergeFunction mergeFunction
+ = [this](const SettingsMergeData &global, const SettingsMergeData &local) {
+ return merge(global, local);
+ };
+ const QVariantMap result = mergeQVariantMaps(main.data, secondary.data, mergeFunction).toMap();
- 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;
+ // Update from the base version to Creator's version.
+ return RestoreData(main.path, postprocessMerge(main.data, secondary.data, result));
}
-SettingsAccessor::RestoreData SettingsAccessor::readSharedSettings(QWidget *parent) const
+/*!
+ * Returns true for housekeeping related keys.
+ */
+bool MergingSettingsAccessor::isHouseKeepingKey(const QString &key) 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
- // 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;
- }
- return sharedSettings;
+ return key == VERSION_KEY || key == ORIGINAL_VERSION_KEY || key == SETTINGS_ID_KEY;
}
-QVariantMap
-SettingsAccessor::mergeSettings(const QVariantMap &userMap, const QVariantMap &sharedMap) const
+QVariantMap MergingSettingsAccessor::postprocessMerge(const QVariantMap &main,
+ const QVariantMap &secondary,
+ const QVariantMap &result) const
{
- QVariantMap newUser = userMap;
- QVariantMap newShared = sharedMap;
-
- const int userVersion = versionFromMap(userMap);
- const int sharedVersion = versionFromMap(sharedMap);
-
- 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;
- }
-
- storeSharedSettings(newShared);
-
- // Update from the base version to Creator's version.
- return upgradeSettings(RestoreData(Utils::FileName::fromLatin1("result"), result), currentVersion()).data;
+ 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
diff --git a/src/libs/utils/settingsaccessor.h b/src/libs/utils/settingsaccessor.h
index 4cf29c7991..974afc0ece 100644
--- a/src/libs/utils/settingsaccessor.h
+++ b/src/libs/utils/settingsaccessor.h
@@ -52,6 +52,20 @@ QTCREATOR_UTILS_EXPORT void setOriginalVersionInMap(QVariantMap &data, int versi
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:
-private:
- RestoreData readSharedSettings(QWidget *parent) const;
+ 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;
- SettingsAccessorPrivate *d;
+ virtual QVariantMap postprocessMerge(const QVariantMap &main, const QVariantMap &secondary,
+ const QVariantMap &result) const;
- friend class SettingsAccessorPrivate;
+private:
+ 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
diff --git a/src/plugins/projectexplorer/userfileaccessor.cpp b/src/plugins/projectexplorer/userfileaccessor.cpp
index 9d33f3c955..02b23ebe58 100644
--- a/src/plugins/projectexplorer/userfileaccessor.cpp
+++ b/src/plugins/projectexplorer/userfileaccessor.cpp
@@ -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() { }
-
- virtual void apply(QVariantMap &userMap, const QString &key, const QVariant &sharedValue) = 0;
+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;
+}
- void synchronize(QVariantMap &userMap, const QVariantMap &sharedMap)
- {
- QVariantMap::const_iterator it = sharedMap.begin();
- QVariantMap::const_iterator eit = sharedMap.end();
+// 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;
+}
- 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
+{
+ 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
{
- project()->setProperty(SHARED_SETTINGS, data);
+ 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);
+
+ TestUserFileAccessor::RestoreData result = accessor.mergeSettings(user, shared);
- QCOMPARE(result.value("OriginalVersion").toInt(), accessor.currentVersion());
- result.remove("OriginalVersion");
- QCOMPARE(result, sharedData);
+ 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
diff --git a/src/plugins/projectexplorer/userfileaccessor.h b/src/plugins/projectexplorer/userfileaccessor.h
index c2060c8d2f..6544de987e 100644
--- a/src/plugins/projectexplorer/userfileaccessor.h
+++ b/src/plugins/projectexplorer/userfileaccessor.h
@@ -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;
};
diff --git a/tests/auto/utils/settings/tst_settings.cpp b/tests/auto/utils/settings/tst_settings.cpp
index 43af7714ea..a0e2870b0b 100644
--- a/tests/auto/utils/settings/tst_settings.cpp
+++ b/tests/auto/utils/settings/tst_settings.cpp
@@ -67,18 +67,38 @@ public:
// BasicTestSettingsAccessor:
// --------------------------------------------------------------------
-class BasicTestSettingsAccessor : public Utils::SettingsAccessor
+class BasicTestSettingsAccessor : public Utils::MergingSettingsAccessor
{
public:
BasicTestSettingsAccessor(const Utils::FileName &baseName = Utils::FileName::fromString("/foo/bar"),
const QByteArray &id = QByteArray(TESTACCESSOR_DEFAULT_ID)) :
- Utils::SettingsAccessor(std::make_unique<Utils::VersionedBackUpStrategy>(this),
- baseName, "TestData", TESTACCESSOR_DN, TESTACCESSOR_APPLICATION_DN)
+ Utils::MergingSettingsAccessor(std::make_unique<Utils::VersionedBackUpStrategy>(this),
+ "TestData", TESTACCESSOR_DN, TESTACCESSOR_APPLICATION_DN)
{
setSettingsId(id);
+ setBaseFilePath(baseName);
}
- using Utils::SettingsAccessor::addVersionUpgrader;
+ SettingsMergeResult merge(const SettingsMergeData &global,
+ const SettingsMergeData &local) const final
+ {
+ Q_UNUSED(global);
+
+ const QString key = local.key;
+ const QVariant main = local.main.value(key);
+ const QVariant secondary = local.secondary.value(key);
+
+ if (isHouseKeepingKey(key))
+ return qMakePair(key, main);
+
+ if (main.isNull() && secondary.isNull())
+ return nullopt;
+ if (!main.isNull())
+ return qMakePair(key, main);
+ return qMakePair(key, secondary);
+ }
+
+ using Utils::MergingSettingsAccessor::addVersionUpgrader;
};
// --------------------------------------------------------------------
@@ -98,7 +118,7 @@ public:
}
// Make methods public for the tests:
- using Utils::SettingsAccessor::upgradeSettings;
+ using Utils::MergingSettingsAccessor::upgradeSettings;
};
// --------------------------------------------------------------------
@@ -607,7 +627,7 @@ void tst_SettingsAccessor::saveSettings()
QVERIFY(accessor.saveSettings(data, nullptr));
PersistentSettingsReader reader;
- QVERIFY(reader.load(testPath(m_tempDir, "saveSettings.user")));
+ QVERIFY(reader.load(testPath(m_tempDir, "saveSettings")));
const QVariantMap read = reader.restoreValues();
@@ -628,7 +648,6 @@ void tst_SettingsAccessor::loadSettings()
const QVariantMap data = versionedMap(6, "loadSettings", generateExtraData());
const Utils::FileName path = testPath(m_tempDir, "loadSettings");
Utils::FileName fullPath = path;
- fullPath.appendString(".user");
PersistentSettingsWriter writer(fullPath, "TestProfile");
QString errorMessage;