20
#include "core/Metadata.h"
22
Merger::Merger(const Database* sourceDb, Database* targetDb)
23
: m_mode(Group::Default)
25
if (!sourceDb || !targetDb) {
26
Q_ASSERT(sourceDb && targetDb);
30
m_context = MergeContext{
31
sourceDb, targetDb, sourceDb->rootGroup(), targetDb->rootGroup(), sourceDb->rootGroup(), targetDb->rootGroup()};
34
Merger::Merger(const Group* sourceGroup, Group* targetGroup)
35
: m_mode(Group::Default)
37
if (!sourceGroup || !targetGroup) {
38
Q_ASSERT(sourceGroup && targetGroup);
42
m_context = MergeContext{sourceGroup->database(),
43
targetGroup->database(),
44
sourceGroup->database()->rootGroup(),
45
targetGroup->database()->rootGroup(),
50
void Merger::setForcedMergeMode(Group::MergeMode mode)
55
void Merger::resetForcedMergeMode()
57
m_mode = Group::Default;
60
void Merger::setSkipDatabaseCustomData(bool state)
62
m_skipCustomData = state;
65
QStringList Merger::merge()
70
changes << mergeGroup(m_context);
71
changes << mergeDeletions(m_context);
72
changes << mergeMetadata(m_context);
75
if (!changes.isEmpty()) {
76
m_context.m_targetDb->markAsModified();
81
Merger::ChangeList Merger::mergeGroup(const MergeContext& context)
85
const QList<Entry*> sourceEntries = context.m_sourceGroup->entries();
86
for (Entry* sourceEntry : sourceEntries) {
87
Entry* targetEntry = context.m_targetRootGroup->findEntryByUuid(sourceEntry->uuid());
89
changes << tr("Creating missing %1 [%2]").arg(sourceEntry->title(), sourceEntry->uuidToHex());
91
targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory);
92
moveEntry(targetEntry, context.m_targetGroup);
95
const bool locationChanged =
96
targetEntry->timeInfo().locationChanged() < sourceEntry->timeInfo().locationChanged();
97
if (locationChanged && targetEntry->group() != context.m_targetGroup) {
98
changes << tr("Relocating %1 [%2]").arg(sourceEntry->title(), sourceEntry->uuidToHex());
99
moveEntry(targetEntry, context.m_targetGroup);
101
changes << resolveEntryConflict(context, sourceEntry, targetEntry);
106
const QList<Group*> sourceChildGroups = context.m_sourceGroup->children();
107
for (Group* sourceChildGroup : sourceChildGroups) {
108
Group* targetChildGroup = context.m_targetRootGroup->findGroupByUuid(sourceChildGroup->uuid());
109
if (!targetChildGroup) {
110
changes << tr("Creating missing %1 [%2]").arg(sourceChildGroup->name(), sourceChildGroup->uuidToHex());
111
targetChildGroup = sourceChildGroup->clone(Entry::CloneNoFlags, Group::CloneNoFlags);
112
moveGroup(targetChildGroup, context.m_targetGroup);
113
TimeInfo timeinfo = targetChildGroup->timeInfo();
114
timeinfo.setLocationChanged(sourceChildGroup->timeInfo().locationChanged());
115
targetChildGroup->setTimeInfo(timeinfo);
117
bool locationChanged =
118
targetChildGroup->timeInfo().locationChanged() < sourceChildGroup->timeInfo().locationChanged();
119
if (locationChanged && targetChildGroup->parent() != context.m_targetGroup) {
120
changes << tr("Relocating %1 [%2]").arg(sourceChildGroup->name(), sourceChildGroup->uuidToHex());
121
moveGroup(targetChildGroup, context.m_targetGroup);
122
TimeInfo timeinfo = targetChildGroup->timeInfo();
123
timeinfo.setLocationChanged(sourceChildGroup->timeInfo().locationChanged());
124
targetChildGroup->setTimeInfo(timeinfo);
126
changes << resolveGroupConflict(context, sourceChildGroup, targetChildGroup);
128
MergeContext subcontext{context.m_sourceDb,
130
context.m_sourceRootGroup,
131
context.m_targetRootGroup,
134
changes << mergeGroup(subcontext);
140
Merger::resolveGroupConflict(const MergeContext& context, const Group* sourceChildGroup, Group* targetChildGroup)
145
const QDateTime timeExisting = targetChildGroup->timeInfo().lastModificationTime();
146
const QDateTime timeOther = sourceChildGroup->timeInfo().lastModificationTime();
149
if (timeExisting < timeOther) {
150
changes << tr("Overwriting %1 [%2]").arg(sourceChildGroup->name(), sourceChildGroup->uuidToHex());
151
targetChildGroup->setName(sourceChildGroup->name());
152
targetChildGroup->setNotes(sourceChildGroup->notes());
153
if (sourceChildGroup->iconNumber() == 0) {
154
targetChildGroup->setIcon(sourceChildGroup->iconUuid());
156
targetChildGroup->setIcon(sourceChildGroup->iconNumber());
158
targetChildGroup->setExpiryTime(sourceChildGroup->timeInfo().expiryTime());
159
TimeInfo timeInfo = targetChildGroup->timeInfo();
160
timeInfo.setLastModificationTime(timeOther);
161
targetChildGroup->setTimeInfo(timeInfo);
166
void Merger::moveEntry(Entry* entry, Group* targetGroup)
169
Group* sourceGroup = entry->group();
170
if (sourceGroup == targetGroup) {
173
const bool sourceGroupUpdateTimeInfo = sourceGroup ? sourceGroup->canUpdateTimeinfo() : false;
175
sourceGroup->setUpdateTimeinfo(false);
177
const bool targetGroupUpdateTimeInfo = targetGroup ? targetGroup->canUpdateTimeinfo() : false;
179
targetGroup->setUpdateTimeinfo(false);
181
const bool entryUpdateTimeInfo = entry->canUpdateTimeinfo();
182
entry->setUpdateTimeinfo(false);
184
entry->setGroup(targetGroup);
186
entry->setUpdateTimeinfo(entryUpdateTimeInfo);
188
targetGroup->setUpdateTimeinfo(targetGroupUpdateTimeInfo);
191
sourceGroup->setUpdateTimeinfo(sourceGroupUpdateTimeInfo);
195
void Merger::moveGroup(Group* group, Group* targetGroup)
198
Group* sourceGroup = group->parentGroup();
199
if (sourceGroup == targetGroup) {
202
const bool sourceGroupUpdateTimeInfo = sourceGroup ? sourceGroup->canUpdateTimeinfo() : false;
204
sourceGroup->setUpdateTimeinfo(false);
206
const bool targetGroupUpdateTimeInfo = targetGroup ? targetGroup->canUpdateTimeinfo() : false;
208
targetGroup->setUpdateTimeinfo(false);
210
const bool groupUpdateTimeInfo = group->canUpdateTimeinfo();
211
group->setUpdateTimeinfo(false);
213
group->setParent(targetGroup);
215
group->setUpdateTimeinfo(groupUpdateTimeInfo);
217
targetGroup->setUpdateTimeinfo(targetGroupUpdateTimeInfo);
220
sourceGroup->setUpdateTimeinfo(sourceGroupUpdateTimeInfo);
224
void Merger::eraseEntry(Entry* entry)
226
Database* database = entry->database();
228
const QList<DeletedObject> deletions = database->deletedObjects();
229
Group* parentGroup = entry->group();
230
const bool groupUpdateTimeInfo = parentGroup ? parentGroup->canUpdateTimeinfo() : false;
232
parentGroup->setUpdateTimeinfo(false);
236
parentGroup->setUpdateTimeinfo(groupUpdateTimeInfo);
238
database->setDeletedObjects(deletions);
241
void Merger::eraseGroup(Group* group)
243
Database* database = group->database();
245
const QList<DeletedObject> deletions = database->deletedObjects();
246
Group* parentGroup = group->parentGroup();
247
const bool groupUpdateTimeInfo = parentGroup ? parentGroup->canUpdateTimeinfo() : false;
249
parentGroup->setUpdateTimeinfo(false);
253
parentGroup->setUpdateTimeinfo(groupUpdateTimeInfo);
255
database->setDeletedObjects(deletions);
258
Merger::ChangeList Merger::resolveEntryConflict_MergeHistories(const MergeContext& context,
259
const Entry* sourceEntry,
261
Group::MergeMode mergeMethod)
266
const int comparison = compare(targetEntry->timeInfo().lastModificationTime(),
267
sourceEntry->timeInfo().lastModificationTime(),
268
CompareItemIgnoreMilliseconds);
269
const int maxItems = targetEntry->database()->metadata()->historyMaxItems();
270
if (comparison < 0) {
271
Group* currentGroup = targetEntry->group();
272
Entry* clonedEntry = sourceEntry->clone(Entry::CloneIncludeHistory);
273
qDebug("Merge %s/%s with alien on top under %s",
274
qPrintable(targetEntry->title()),
275
qPrintable(sourceEntry->title()),
276
qPrintable(currentGroup->name()));
277
changes << tr("Synchronizing from newer source %1 [%2]").arg(targetEntry->title(), targetEntry->uuidToHex());
278
mergeHistory(targetEntry, clonedEntry, mergeMethod, maxItems);
279
eraseEntry(targetEntry);
280
moveEntry(clonedEntry, currentGroup);
282
qDebug("Merge %s/%s with local on top/under %s",
283
qPrintable(targetEntry->title()),
284
qPrintable(sourceEntry->title()),
285
qPrintable(targetEntry->group()->name()));
286
const bool changed = mergeHistory(sourceEntry, targetEntry, mergeMethod, maxItems);
289
<< tr("Synchronizing from older source %1 [%2]").arg(targetEntry->title(), targetEntry->uuidToHex());
296
Merger::resolveEntryConflict(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry)
302
Group::MergeMode mergeMode = m_mode == Group::Default ? context.m_targetGroup->mergeMode() : m_mode;
303
return resolveEntryConflict_MergeHistories(context, sourceEntry, targetEntry, mergeMode);
306
bool Merger::mergeHistory(const Entry* sourceEntry,
308
Group::MergeMode mergeMethod,
311
Q_UNUSED(mergeMethod);
312
const auto targetHistoryItems = targetEntry->historyItems();
313
const auto sourceHistoryItems = sourceEntry->historyItems();
314
const int comparison = compare(sourceEntry->timeInfo().lastModificationTime(),
315
targetEntry->timeInfo().lastModificationTime(),
316
CompareItemIgnoreMilliseconds);
317
const bool preferLocal = comparison < 0;
318
const bool preferRemote = comparison > 0;
320
QMap<QDateTime, Entry*> merged;
321
for (Entry* historyItem : targetHistoryItems) {
322
const QDateTime modificationTime = Clock::serialized(historyItem->timeInfo().lastModificationTime());
323
if (merged.contains(modificationTime)
324
&& !merged[modificationTime]->equals(historyItem, CompareItemIgnoreMilliseconds)) {
325
::qWarning("Inconsistent history entry of %s[%s] at %s contains conflicting changes - conflict resolution "
327
qPrintable(sourceEntry->title()),
328
qPrintable(sourceEntry->uuidToHex()),
329
qPrintable(modificationTime.toString("yyyy-MM-dd HH-mm-ss-zzz")));
331
merged[modificationTime] = historyItem->clone(Entry::CloneNoFlags);
333
for (Entry* historyItem : sourceHistoryItems) {
335
const QDateTime modificationTime = Clock::serialized(historyItem->timeInfo().lastModificationTime());
336
if (merged.contains(modificationTime)
337
&& !merged[modificationTime]->equals(historyItem, CompareItemIgnoreMilliseconds)) {
339
"History entry of %s[%s] at %s contains conflicting changes - conflict resolution may lose data!",
340
qPrintable(sourceEntry->title()),
341
qPrintable(sourceEntry->uuidToHex()),
342
qPrintable(modificationTime.toString("yyyy-MM-dd HH-mm-ss-zzz")));
344
if (preferRemote && merged.contains(modificationTime)) {
346
delete merged.take(modificationTime);
348
if (!merged.contains(modificationTime)) {
349
merged[modificationTime] = historyItem->clone(Entry::CloneNoFlags);
353
const QDateTime targetModificationTime = Clock::serialized(targetEntry->timeInfo().lastModificationTime());
354
const QDateTime sourceModificationTime = Clock::serialized(sourceEntry->timeInfo().lastModificationTime());
355
if (targetModificationTime == sourceModificationTime
356
&& !targetEntry->equals(sourceEntry,
357
CompareItemIgnoreMilliseconds | CompareItemIgnoreHistory | CompareItemIgnoreLocation)) {
358
::qWarning("Entry of %s[%s] contains conflicting changes - conflict resolution may lose data!",
359
qPrintable(sourceEntry->title()),
360
qPrintable(sourceEntry->uuidToHex()));
363
if (targetModificationTime < sourceModificationTime) {
364
if (preferLocal && merged.contains(targetModificationTime)) {
366
delete merged.take(targetModificationTime);
368
if (!merged.contains(targetModificationTime)) {
369
merged[targetModificationTime] = targetEntry->clone(Entry::CloneNoFlags);
371
} else if (targetModificationTime > sourceModificationTime) {
372
if (preferRemote && !merged.contains(sourceModificationTime)) {
374
delete merged.take(sourceModificationTime);
376
if (!merged.contains(sourceModificationTime)) {
377
merged[sourceModificationTime] = sourceEntry->clone(Entry::CloneNoFlags);
381
bool changed = false;
382
const auto updatedHistoryItems = merged.values();
383
for (int i = 0; i < maxItems; ++i) {
384
const Entry* oldEntry = targetHistoryItems.value(targetHistoryItems.count() - i);
385
const Entry* newEntry = updatedHistoryItems.value(updatedHistoryItems.count() - i);
386
if (!oldEntry && !newEntry) {
389
if (oldEntry && newEntry && oldEntry->equals(newEntry, CompareItemIgnoreMilliseconds)) {
396
qDeleteAll(updatedHistoryItems);
401
const TimeInfo timeInfo = targetEntry->timeInfo();
402
const bool blockedSignals = targetEntry->blockSignals(true);
403
bool updateTimeInfo = targetEntry->canUpdateTimeinfo();
404
targetEntry->setUpdateTimeinfo(false);
405
targetEntry->removeHistoryItems(targetHistoryItems);
406
for (Entry* historyItem : merged) {
407
Q_ASSERT(!historyItem->parent());
408
targetEntry->addHistoryItem(historyItem);
410
targetEntry->truncateHistory();
411
targetEntry->blockSignals(blockedSignals);
412
targetEntry->setUpdateTimeinfo(updateTimeInfo);
413
Q_ASSERT(timeInfo == targetEntry->timeInfo());
418
Merger::ChangeList Merger::mergeDeletions(const MergeContext& context)
421
Group::MergeMode mergeMode = m_mode == Group::Default ? context.m_targetGroup->mergeMode() : m_mode;
422
if (mergeMode != Group::Synchronize) {
427
const auto targetDeletions = context.m_targetDb->deletedObjects();
428
const auto sourceDeletions = context.m_sourceDb->deletedObjects();
430
QList<DeletedObject> deletions;
431
QMap<QUuid, DeletedObject> mergedDeletions;
432
QList<Entry*> entries;
433
QList<Group*> groups;
435
for (const auto& object : (targetDeletions + sourceDeletions)) {
436
if (!mergedDeletions.contains(object.uuid)) {
437
mergedDeletions[object.uuid] = object;
439
auto* entry = context.m_targetRootGroup->findEntryByUuid(object.uuid);
444
auto* group = context.m_targetRootGroup->findGroupByUuid(object.uuid);
452
if (mergedDeletions[object.uuid].deletionTime > object.deletionTime) {
453
mergedDeletions[object.uuid] = object;
457
while (!entries.isEmpty()) {
458
auto* entry = entries.takeFirst();
459
const auto& object = mergedDeletions[entry->uuid()];
460
if (entry->timeInfo().lastModificationTime() > object.deletionTime) {
465
if (entry->group()) {
466
changes << tr("Deleting child %1 [%2]").arg(entry->title(), entry->uuidToHex());
468
changes << tr("Deleting orphan %1 [%2]").arg(entry->title(), entry->uuidToHex());
474
while (!groups.isEmpty()) {
475
auto* group = groups.takeFirst();
476
if (!(group->children().toSet() & groups.toSet()).isEmpty()) {
481
const auto& object = mergedDeletions[group->uuid()];
482
if (group->timeInfo().lastModificationTime() > object.deletionTime) {
486
if (!group->entriesRecursive(false).isEmpty() || !group->groupsRecursive(false).isEmpty()) {
491
if (group->parentGroup()) {
492
changes << tr("Deleting child %1 [%2]").arg(group->name(), group->uuidToHex());
494
changes << tr("Deleting orphan %1 [%2]").arg(group->name(), group->uuidToHex());
499
if (deletions != context.m_targetDb->deletedObjects()) {
500
changes << tr("Changed deleted objects");
502
context.m_targetDb->setDeletedObjects(deletions);
506
Merger::ChangeList Merger::mergeMetadata(const MergeContext& context)
512
auto* sourceMetadata = context.m_sourceDb->metadata();
513
auto* targetMetadata = context.m_targetDb->metadata();
515
for (const auto& iconUuid : sourceMetadata->customIconsOrder()) {
516
if (!targetMetadata->hasCustomIcon(iconUuid)) {
517
targetMetadata->addCustomIcon(iconUuid, sourceMetadata->customIcon(iconUuid));
518
changes << tr("Adding missing icon %1").arg(QString::fromLatin1(iconUuid.toRfc4122().toHex()));
523
if (m_skipCustomData) {
528
const auto targetCustomDataModificationTime = targetMetadata->customData()->lastModified();
529
const auto sourceCustomDataModificationTime = sourceMetadata->customData()->lastModified();
530
if (!targetMetadata->customData()->contains(CustomData::LastModified)
531
|| (targetCustomDataModificationTime.isValid() && sourceCustomDataModificationTime.isValid()
532
&& targetCustomDataModificationTime < sourceCustomDataModificationTime)) {
533
const auto sourceCustomDataKeys = sourceMetadata->customData()->keys();
534
const auto targetCustomDataKeys = targetMetadata->customData()->keys();
537
for (const auto& key : targetCustomDataKeys) {
539
if (!sourceMetadata->customData()->contains(key) && !sourceMetadata->customData()->isProtected(key)) {
540
auto value = targetMetadata->customData()->value(key);
541
targetMetadata->customData()->remove(key);
542
changes << tr("Removed custom data %1 [%2]").arg(key, value);
547
for (const auto& key : sourceCustomDataKeys) {
549
if (sourceMetadata->customData()->isAutoGenerated(key)) {
553
auto sourceValue = sourceMetadata->customData()->value(key);
554
auto targetValue = targetMetadata->customData()->value(key);
556
if (sourceValue != targetValue) {
557
targetMetadata->customData()->set(key, sourceValue);
558
changes << tr("Adding custom data %1 [%2]").arg(key, sourceValue);