keepassxc

Форк
0
/
Merger.cpp 
564 строки · 23.4 Кб
1
/*
2
 *  Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
3
 *
4
 *  This program is free software: you can redistribute it and/or modify
5
 *  it under the terms of the GNU General Public License as published by
6
 *  the Free Software Foundation, either version 2 or (at your option)
7
 *  version 3 of the License.
8
 *
9
 *  This program is distributed in the hope that it will be useful,
10
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 *  GNU General Public License for more details.
13
 *
14
 *  You should have received a copy of the GNU General Public License
15
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
 */
17

18
#include "Merger.h"
19

20
#include "core/Metadata.h"
21

22
Merger::Merger(const Database* sourceDb, Database* targetDb)
23
    : m_mode(Group::Default)
24
{
25
    if (!sourceDb || !targetDb) {
26
        Q_ASSERT(sourceDb && targetDb);
27
        return;
28
    }
29

30
    m_context = MergeContext{
31
        sourceDb, targetDb, sourceDb->rootGroup(), targetDb->rootGroup(), sourceDb->rootGroup(), targetDb->rootGroup()};
32
}
33

34
Merger::Merger(const Group* sourceGroup, Group* targetGroup)
35
    : m_mode(Group::Default)
36
{
37
    if (!sourceGroup || !targetGroup) {
38
        Q_ASSERT(sourceGroup && targetGroup);
39
        return;
40
    }
41

42
    m_context = MergeContext{sourceGroup->database(),
43
                             targetGroup->database(),
44
                             sourceGroup->database()->rootGroup(),
45
                             targetGroup->database()->rootGroup(),
46
                             sourceGroup,
47
                             targetGroup};
48
}
49

50
void Merger::setForcedMergeMode(Group::MergeMode mode)
51
{
52
    m_mode = mode;
53
}
54

55
void Merger::resetForcedMergeMode()
56
{
57
    m_mode = Group::Default;
58
}
59

60
void Merger::setSkipDatabaseCustomData(bool state)
61
{
62
    m_skipCustomData = state;
63
}
64

65
QStringList Merger::merge()
66
{
67
    // Order of merge steps is important - it is possible that we
68
    // create some items before deleting them afterwards
69
    ChangeList changes;
70
    changes << mergeGroup(m_context);
71
    changes << mergeDeletions(m_context);
72
    changes << mergeMetadata(m_context);
73

74
    // At this point we have a list of changes we may want to show the user
75
    if (!changes.isEmpty()) {
76
        m_context.m_targetDb->markAsModified();
77
    }
78
    return changes;
79
}
80

81
Merger::ChangeList Merger::mergeGroup(const MergeContext& context)
82
{
83
    ChangeList changes;
84
    // merge entries
85
    const QList<Entry*> sourceEntries = context.m_sourceGroup->entries();
86
    for (Entry* sourceEntry : sourceEntries) {
87
        Entry* targetEntry = context.m_targetRootGroup->findEntryByUuid(sourceEntry->uuid());
88
        if (!targetEntry) {
89
            changes << tr("Creating missing %1 [%2]").arg(sourceEntry->title(), sourceEntry->uuidToHex());
90
            // This entry does not exist at all. Create it.
91
            targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory);
92
            moveEntry(targetEntry, context.m_targetGroup);
93
        } else {
94
            // Entry is already present in the database. Update it.
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);
100
            }
101
            changes << resolveEntryConflict(context, sourceEntry, targetEntry);
102
        }
103
    }
104

105
    // merge groups recursively
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);
116
        } else {
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);
125
            }
126
            changes << resolveGroupConflict(context, sourceChildGroup, targetChildGroup);
127
        }
128
        MergeContext subcontext{context.m_sourceDb,
129
                                context.m_targetDb,
130
                                context.m_sourceRootGroup,
131
                                context.m_targetRootGroup,
132
                                sourceChildGroup,
133
                                targetChildGroup};
134
        changes << mergeGroup(subcontext);
135
    }
136
    return changes;
137
}
138

139
Merger::ChangeList
140
Merger::resolveGroupConflict(const MergeContext& context, const Group* sourceChildGroup, Group* targetChildGroup)
141
{
142
    Q_UNUSED(context);
143
    ChangeList changes;
144

145
    const QDateTime timeExisting = targetChildGroup->timeInfo().lastModificationTime();
146
    const QDateTime timeOther = sourceChildGroup->timeInfo().lastModificationTime();
147

148
    // only if the other group is newer, update the existing one.
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());
155
        } else {
156
            targetChildGroup->setIcon(sourceChildGroup->iconNumber());
157
        }
158
        targetChildGroup->setExpiryTime(sourceChildGroup->timeInfo().expiryTime());
159
        TimeInfo timeInfo = targetChildGroup->timeInfo();
160
        timeInfo.setLastModificationTime(timeOther);
161
        targetChildGroup->setTimeInfo(timeInfo);
162
    }
163
    return changes;
164
}
165

166
void Merger::moveEntry(Entry* entry, Group* targetGroup)
167
{
168
    Q_ASSERT(entry);
169
    Group* sourceGroup = entry->group();
170
    if (sourceGroup == targetGroup) {
171
        return;
172
    }
173
    const bool sourceGroupUpdateTimeInfo = sourceGroup ? sourceGroup->canUpdateTimeinfo() : false;
174
    if (sourceGroup) {
175
        sourceGroup->setUpdateTimeinfo(false);
176
    }
177
    const bool targetGroupUpdateTimeInfo = targetGroup ? targetGroup->canUpdateTimeinfo() : false;
178
    if (targetGroup) {
179
        targetGroup->setUpdateTimeinfo(false);
180
    }
181
    const bool entryUpdateTimeInfo = entry->canUpdateTimeinfo();
182
    entry->setUpdateTimeinfo(false);
183

184
    entry->setGroup(targetGroup);
185

186
    entry->setUpdateTimeinfo(entryUpdateTimeInfo);
187
    if (targetGroup) {
188
        targetGroup->setUpdateTimeinfo(targetGroupUpdateTimeInfo);
189
    }
190
    if (sourceGroup) {
191
        sourceGroup->setUpdateTimeinfo(sourceGroupUpdateTimeInfo);
192
    }
193
}
194

195
void Merger::moveGroup(Group* group, Group* targetGroup)
196
{
197
    Q_ASSERT(group);
198
    Group* sourceGroup = group->parentGroup();
199
    if (sourceGroup == targetGroup) {
200
        return;
201
    }
202
    const bool sourceGroupUpdateTimeInfo = sourceGroup ? sourceGroup->canUpdateTimeinfo() : false;
203
    if (sourceGroup) {
204
        sourceGroup->setUpdateTimeinfo(false);
205
    }
206
    const bool targetGroupUpdateTimeInfo = targetGroup ? targetGroup->canUpdateTimeinfo() : false;
207
    if (targetGroup) {
208
        targetGroup->setUpdateTimeinfo(false);
209
    }
210
    const bool groupUpdateTimeInfo = group->canUpdateTimeinfo();
211
    group->setUpdateTimeinfo(false);
212

213
    group->setParent(targetGroup);
214

215
    group->setUpdateTimeinfo(groupUpdateTimeInfo);
216
    if (targetGroup) {
217
        targetGroup->setUpdateTimeinfo(targetGroupUpdateTimeInfo);
218
    }
219
    if (sourceGroup) {
220
        sourceGroup->setUpdateTimeinfo(sourceGroupUpdateTimeInfo);
221
    }
222
}
223

224
void Merger::eraseEntry(Entry* entry)
225
{
226
    Database* database = entry->database();
227
    // most simple method to remove an item from DeletedObjects :(
228
    const QList<DeletedObject> deletions = database->deletedObjects();
229
    Group* parentGroup = entry->group();
230
    const bool groupUpdateTimeInfo = parentGroup ? parentGroup->canUpdateTimeinfo() : false;
231
    if (parentGroup) {
232
        parentGroup->setUpdateTimeinfo(false);
233
    }
234
    delete entry;
235
    if (parentGroup) {
236
        parentGroup->setUpdateTimeinfo(groupUpdateTimeInfo);
237
    }
238
    database->setDeletedObjects(deletions);
239
}
240

241
void Merger::eraseGroup(Group* group)
242
{
243
    Database* database = group->database();
244
    // most simple method to remove an item from DeletedObjects :(
245
    const QList<DeletedObject> deletions = database->deletedObjects();
246
    Group* parentGroup = group->parentGroup();
247
    const bool groupUpdateTimeInfo = parentGroup ? parentGroup->canUpdateTimeinfo() : false;
248
    if (parentGroup) {
249
        parentGroup->setUpdateTimeinfo(false);
250
    }
251
    delete group;
252
    if (parentGroup) {
253
        parentGroup->setUpdateTimeinfo(groupUpdateTimeInfo);
254
    }
255
    database->setDeletedObjects(deletions);
256
}
257

258
Merger::ChangeList Merger::resolveEntryConflict_MergeHistories(const MergeContext& context,
259
                                                               const Entry* sourceEntry,
260
                                                               Entry* targetEntry,
261
                                                               Group::MergeMode mergeMethod)
262
{
263
    Q_UNUSED(context);
264

265
    ChangeList changes;
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);
281
    } else {
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);
287
        if (changed) {
288
            changes
289
                << tr("Synchronizing from older source %1 [%2]").arg(targetEntry->title(), targetEntry->uuidToHex());
290
        }
291
    }
292
    return changes;
293
}
294

295
Merger::ChangeList
296
Merger::resolveEntryConflict(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry)
297
{
298
    // We need to cut off the milliseconds since the persistent format only supports times down to seconds
299
    // so when we import data from a remote source, it may represent the (or even some msec newer) data
300
    // which may be discarded due to higher runtime precision
301

302
    Group::MergeMode mergeMode = m_mode == Group::Default ? context.m_targetGroup->mergeMode() : m_mode;
303
    return resolveEntryConflict_MergeHistories(context, sourceEntry, targetEntry, mergeMode);
304
}
305

306
bool Merger::mergeHistory(const Entry* sourceEntry,
307
                          Entry* targetEntry,
308
                          Group::MergeMode mergeMethod,
309
                          const int maxItems)
310
{
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;
319

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 "
326
                       "may lose data!",
327
                       qPrintable(sourceEntry->title()),
328
                       qPrintable(sourceEntry->uuidToHex()),
329
                       qPrintable(modificationTime.toString("yyyy-MM-dd HH-mm-ss-zzz")));
330
        }
331
        merged[modificationTime] = historyItem->clone(Entry::CloneNoFlags);
332
    }
333
    for (Entry* historyItem : sourceHistoryItems) {
334
        // Items with same modification-time changes will be regarded as same (like KeePass2)
335
        const QDateTime modificationTime = Clock::serialized(historyItem->timeInfo().lastModificationTime());
336
        if (merged.contains(modificationTime)
337
            && !merged[modificationTime]->equals(historyItem, CompareItemIgnoreMilliseconds)) {
338
            ::qWarning(
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")));
343
        }
344
        if (preferRemote && merged.contains(modificationTime)) {
345
            // forcefully apply the remote history item
346
            delete merged.take(modificationTime);
347
        }
348
        if (!merged.contains(modificationTime)) {
349
            merged[modificationTime] = historyItem->clone(Entry::CloneNoFlags);
350
        }
351
    }
352

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()));
361
    }
362

363
    if (targetModificationTime < sourceModificationTime) {
364
        if (preferLocal && merged.contains(targetModificationTime)) {
365
            // forcefully apply the local history item
366
            delete merged.take(targetModificationTime);
367
        }
368
        if (!merged.contains(targetModificationTime)) {
369
            merged[targetModificationTime] = targetEntry->clone(Entry::CloneNoFlags);
370
        }
371
    } else if (targetModificationTime > sourceModificationTime) {
372
        if (preferRemote && !merged.contains(sourceModificationTime)) {
373
            // forcefully apply the remote history item
374
            delete merged.take(sourceModificationTime);
375
        }
376
        if (!merged.contains(sourceModificationTime)) {
377
            merged[sourceModificationTime] = sourceEntry->clone(Entry::CloneNoFlags);
378
        }
379
    }
380

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) {
387
            continue;
388
        }
389
        if (oldEntry && newEntry && oldEntry->equals(newEntry, CompareItemIgnoreMilliseconds)) {
390
            continue;
391
        }
392
        changed = true;
393
        break;
394
    }
395
    if (!changed) {
396
        qDeleteAll(updatedHistoryItems);
397
        return false;
398
    }
399
    // We need to prevent any modification to the database since every change should be tracked either
400
    // in a clone history item or in the Entry itself
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);
409
    }
410
    targetEntry->truncateHistory();
411
    targetEntry->blockSignals(blockedSignals);
412
    targetEntry->setUpdateTimeinfo(updateTimeInfo);
413
    Q_ASSERT(timeInfo == targetEntry->timeInfo());
414
    Q_UNUSED(timeInfo);
415
    return true;
416
}
417

418
Merger::ChangeList Merger::mergeDeletions(const MergeContext& context)
419
{
420
    ChangeList changes;
421
    Group::MergeMode mergeMode = m_mode == Group::Default ? context.m_targetGroup->mergeMode() : m_mode;
422
    if (mergeMode != Group::Synchronize) {
423
        // no deletions are applied for any other strategy!
424
        return changes;
425
    }
426

427
    const auto targetDeletions = context.m_targetDb->deletedObjects();
428
    const auto sourceDeletions = context.m_sourceDb->deletedObjects();
429

430
    QList<DeletedObject> deletions;
431
    QMap<QUuid, DeletedObject> mergedDeletions;
432
    QList<Entry*> entries;
433
    QList<Group*> groups;
434

435
    for (const auto& object : (targetDeletions + sourceDeletions)) {
436
        if (!mergedDeletions.contains(object.uuid)) {
437
            mergedDeletions[object.uuid] = object;
438

439
            auto* entry = context.m_targetRootGroup->findEntryByUuid(object.uuid);
440
            if (entry) {
441
                entries << entry;
442
                continue;
443
            }
444
            auto* group = context.m_targetRootGroup->findGroupByUuid(object.uuid);
445
            if (group) {
446
                groups << group;
447
                continue;
448
            }
449
            deletions << object;
450
            continue;
451
        }
452
        if (mergedDeletions[object.uuid].deletionTime > object.deletionTime) {
453
            mergedDeletions[object.uuid] = object;
454
        }
455
    }
456

457
    while (!entries.isEmpty()) {
458
        auto* entry = entries.takeFirst();
459
        const auto& object = mergedDeletions[entry->uuid()];
460
        if (entry->timeInfo().lastModificationTime() > object.deletionTime) {
461
            // keep deleted entry since it was changed after deletion date
462
            continue;
463
        }
464
        deletions << object;
465
        if (entry->group()) {
466
            changes << tr("Deleting child %1 [%2]").arg(entry->title(), entry->uuidToHex());
467
        } else {
468
            changes << tr("Deleting orphan %1 [%2]").arg(entry->title(), entry->uuidToHex());
469
        }
470
        // Entry is inserted into deletedObjects after deletions are processed
471
        eraseEntry(entry);
472
    }
473

474
    while (!groups.isEmpty()) {
475
        auto* group = groups.takeFirst();
476
        if (!(group->children().toSet() & groups.toSet()).isEmpty()) {
477
            // we need to finish all children before we are able to determine if the group can be removed
478
            groups << group;
479
            continue;
480
        }
481
        const auto& object = mergedDeletions[group->uuid()];
482
        if (group->timeInfo().lastModificationTime() > object.deletionTime) {
483
            // keep deleted group since it was changed after deletion date
484
            continue;
485
        }
486
        if (!group->entriesRecursive(false).isEmpty() || !group->groupsRecursive(false).isEmpty()) {
487
            // keep deleted group since it contains undeleted content
488
            continue;
489
        }
490
        deletions << object;
491
        if (group->parentGroup()) {
492
            changes << tr("Deleting child %1 [%2]").arg(group->name(), group->uuidToHex());
493
        } else {
494
            changes << tr("Deleting orphan %1 [%2]").arg(group->name(), group->uuidToHex());
495
        }
496
        eraseGroup(group);
497
    }
498
    // Put every deletion to the earliest date of deletion
499
    if (deletions != context.m_targetDb->deletedObjects()) {
500
        changes << tr("Changed deleted objects");
501
    }
502
    context.m_targetDb->setDeletedObjects(deletions);
503
    return changes;
504
}
505

506
Merger::ChangeList Merger::mergeMetadata(const MergeContext& context)
507
{
508
    // TODO HNH: missing handling of recycle bin, names, templates for groups and entries,
509
    //           public data (entries of newer dict override keys of older dict - ignoring
510
    //           their own age - it is enough if one entry of the whole dict is newer) => possible lost update
511
    ChangeList changes;
512
    auto* sourceMetadata = context.m_sourceDb->metadata();
513
    auto* targetMetadata = context.m_targetDb->metadata();
514

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()));
519
        }
520
    }
521

522
    // Some merges shouldn't modify the database custom data
523
    if (m_skipCustomData) {
524
        return changes;
525
    }
526

527
    // Merge Custom Data if source is newer
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();
535

536
        // Check missing keys from source. Remove those from target
537
        for (const auto& key : targetCustomDataKeys) {
538
            // Do not remove protected custom data
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);
543
            }
544
        }
545

546
        // Transfer new/existing keys
547
        for (const auto& key : sourceCustomDataKeys) {
548
            // Don't merge auto-generated keys
549
            if (sourceMetadata->customData()->isAutoGenerated(key)) {
550
                continue;
551
            }
552

553
            auto sourceValue = sourceMetadata->customData()->value(key);
554
            auto targetValue = targetMetadata->customData()->value(key);
555
            // Merge only if the values are not the same.
556
            if (sourceValue != targetValue) {
557
                targetMetadata->customData()->set(key, sourceValue);
558
                changes << tr("Adding custom data %1 [%2]").arg(key, sourceValue);
559
            }
560
        }
561
    }
562

563
    return changes;
564
}
565

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.