diff --git a/src/libs/utils/settingsaccessor.cpp b/src/libs/utils/settingsaccessor.cpp index 613c0bde2ee..766f2eac9db 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 +/*! + * 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 &&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 m_sharedFile; -}; - -// Return path to shared directory for .user files, create if necessary. -static inline Utils::optional 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 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 &&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(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 &&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 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> mergeResult = merge(global, local); + if (!mergeResult) + continue; + + QPair 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 4cf29c79915..974afc0ece1 100644 --- a/src/libs/utils/settingsaccessor.h +++ b/src/libs/utils/settingsaccessor.h @@ -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>; + // -------------------------------------------------------------------- // 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 &&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 &&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 &&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 m_secondaryAccessor; }; +using SettingsMergeFunction = std::function; +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 9d33f3c9555..02b23ebe58d 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() { } +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 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 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(this), - project->projectFilePath(), "QtCreatorProject", - project->displayName(), Core::Constants::IDE_DISPLAY_NAME), + MergingSettingsAccessor(std::make_unique(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(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 diff --git a/src/plugins/projectexplorer/userfileaccessor.h b/src/plugins/projectexplorer/userfileaccessor.h index c2060c8d2f2..6544de987ea 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 43af7714ea6..a0e2870b0bc 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(this), - baseName, "TestData", TESTACCESSOR_DN, TESTACCESSOR_APPLICATION_DN) + Utils::MergingSettingsAccessor(std::make_unique(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;