summaryrefslogtreecommitdiff
path: root/src/plugins/projectexplorer/userfileaccessor.cpp
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 /src/plugins/projectexplorer/userfileaccessor.cpp
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>
Diffstat (limited to 'src/plugins/projectexplorer/userfileaccessor.cpp')
-rw-r--r--src/plugins/projectexplorer/userfileaccessor.cpp291
1 files changed, 203 insertions, 88 deletions
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