MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 94.7% 179 / 0 / 189
Functions: 100.0% 10 / 0 / 10
Branches: 72.1% 290 / 0 / 402

presentation/QuickContactActionsMerge.cpp
Line Branch Exec Source
1 // Merging contacts (Sprint 97 S97.5.9, Sprint 103 people details) and the
2 // duplicate list: many cards in one undo step. Part of QuickContactActions,
3 // split from QuickContactActionsBulk.cpp to keep both within the unit limits.
4 #include "QuickContactActions.h"
5 #include "ContactMerge.h"
6
7 #include "QuickContactModel.h"
8 #include "app/core/ContactSyncCoordinator.h"
9 #include "data/ContactPeopleMerge.h"
10 #include "data/ContactQueries.h"
11 #include "data/ContactStore.h"
12 #include "data/VCardCodec.h"
13
14 #include <QLoggingCategory>
15 #include <QSet>
16 #include <QSettings>
17 #include <QSqlQuery>
18
19 Q_DECLARE_LOGGING_CATEGORY(lcContactActions)
20
21 namespace {
22
23
3/9
✓ Branch 10 → 11 taken 2 times.
✓ Branch 12 → 13 taken 4 times.
✓ Branch 12 → 14 taken 2 times.
✗ Branch 21 → 22 not taken.
✗ Branch 21 → 23 not taken.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 41 not taken.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
10 QVariantMap failure(const QString &error) { return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), error}}; }
24
25 // The stored cards of `keys`, parsed, in the same order (merge: their people
26 // details); an unreadable card is an empty card with its UID.
27 13 QList<ContactCard> parsedCards(ContactSyncCoordinator *sync, const QStringList &keys) {
28 13 QList<ContactCard> cards;
29
2/2
✓ Branch 26 → 4 taken 35 times.
✓ Branch 26 → 27 taken 13 times.
48 for (const QString &key : keys) {
30
1/1
✓ Branch 5 → 6 taken 35 times.
35 ContactCard card;
31 35 qint64 bookId = 0;
32 35 QString uid;
33
4/7
✓ Branch 6 → 7 taken 35 times.
✗ Branch 6 → 10 not taken.
✓ Branch 7 → 8 taken 35 times.
✓ Branch 8 → 9 taken 35 times.
✗ Branch 8 → 10 not taken.
✓ Branch 11 → 12 taken 35 times.
✗ Branch 11 → 20 not taken.
35 if (sync && ContactQueries::splitKey(key, &bookId, &uid))
34
2/3
✓ Branch 13 → 14 taken 35 times.
✓ Branch 15 → 16 taken 35 times.
✗ Branch 15 → 18 not taken.
35 if (const auto stored = sync->store()->card(bookId, uid))
35
1/1
✓ Branch 17 → 18 taken 35 times.
35 VCardCodec::parse(stored->raw, &card);
36 35 card.uid = uid;
37
1/1
✓ Branch 21 → 22 taken 35 times.
35 cards.append(card);
38 35 }
39 13 return cards;
40 ✗ }
41
42 } // namespace
43
44 102 QVariantList QuickContactActions::possibleDuplicates(const QString &key) const {
45 102 ContactSyncCoordinator *sync = m_coordinator();
46 102 QVariantList result;
47
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 102 times.
102 if (!sync)
48 ✗ return result;
49
6/6
✓ Branch 6 → 7 taken 102 times.
✓ Branch 7 → 8 taken 102 times.
✓ Branch 9 → 10 taken 102 times.
✓ Branch 10 → 11 taken 102 times.
✓ Branch 52 → 12 taken 2 times.
✓ Branch 52 → 53 taken 102 times.
104 for (const ContactQueries::Duplicate &duplicate : ContactQueries::duplicatesOf(sync->store()->database(), key))
50
1/1
✓ Branch 35 → 36 taken 2 times.
6 result.append(QVariantMap{{QStringLiteral("key"), duplicate.key},
51 4 {QStringLiteral("displayName"), duplicate.displayName},
52
1/3
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 2 times.
✗ Branch 26 → 28 not taken.
4 {QStringLiteral("bookName"), duplicate.bookName.isEmpty() ? tr("On this computer")
53 2 : duplicate.bookName},
54
2/4
✓ Branch 39 → 40 taken 8 times.
✓ Branch 39 → 41 taken 2 times.
✗ Branch 64 → 65 not taken.
✗ Branch 64 → 66 not taken.
116 {QStringLiteral("sameBook"), duplicate.sameBook}});
55 102 return result;
56 10 }
57
58 // F100-15: the cards of `keys` (in order) or an error text.
59 16 QString QuickContactActions::mergeCards(const QStringList &keys, QList<QVariantMap> *cards) const {
60
1/3
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 16 times.
✗ Branch 4 → 5 not taken.
16 if (keys.size() < 2) return tr("Select at least two contacts to merge.");
61 16 QVariant bookId;
62
2/2
✓ Branch 106 → 9 taken 41 times.
✓ Branch 106 → 107 taken 13 times.
54 for (const QString &key : keys) {
63
1/1
✓ Branch 10 → 11 taken 41 times.
41 const QVariantMap card = m_model.card(key);
64
5/9
✓ Branch 11 → 12 taken 41 times.
✓ Branch 12 → 13 taken 41 times.
✗ Branch 12 → 16 not taken.
✓ Branch 14 → 15 taken 41 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 41 times.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 21 taken 41 times.
✗ Branch 19 → 20 not taken.
41 if (card.isEmpty() || key.startsWith(QLatin1String("collected:"))) return tr("Only contacts of an address book can be merged.");
65
3/5
✓ Branch 26 → 27 taken 41 times.
✓ Branch 27 → 28 taken 41 times.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 37 taken 41 times.
✗ Branch 35 → 36 not taken.
82 if (card.value(QStringLiteral("kind")).toString() == QLatin1String("group")) return tr("Groups cannot be merged.");
66 // S106-264: a family entry is no person — merging folded its members
67 // into a person's card.
68
5/5
✓ Branch 42 → 43 taken 41 times.
✓ Branch 43 → 44 taken 41 times.
✓ Branch 50 → 51 taken 1 time.
✓ Branch 50 → 53 taken 40 times.
✓ Branch 51 → 52 taken 1 time.
82 if (card.value(QStringLiteral("kind")).toString() == QLatin1String("family")) return tr("Families cannot be merged.");
69
17/25
✓ Branch 53 → 54 taken 40 times.
✓ Branch 54 → 55 taken 24 times.
✓ Branch 54 → 63 taken 16 times.
✓ Branch 59 → 60 taken 24 times.
✓ Branch 60 → 61 taken 24 times.
✓ Branch 61 → 62 taken 2 times.
✓ Branch 61 → 63 taken 22 times.
✓ Branch 64 → 65 taken 24 times.
✓ Branch 64 → 66 taken 16 times.
✓ Branch 66 → 67 taken 24 times.
✓ Branch 66 → 68 taken 16 times.
✓ Branch 68 → 69 taken 24 times.
✓ Branch 68 → 70 taken 16 times.
✓ Branch 70 → 71 taken 24 times.
✓ Branch 70 → 72 taken 16 times.
✓ Branch 72 → 73 taken 2 times.
✓ Branch 72 → 75 taken 38 times.
✗ Branch 140 → 141 not taken.
✗ Branch 140 → 142 not taken.
✗ Branch 144 → 145 not taken.
✗ Branch 144 → 146 not taken.
✗ Branch 148 → 149 not taken.
✗ Branch 148 → 150 not taken.
✗ Branch 152 → 153 not taken.
✗ Branch 152 → 154 not taken.
64 if (bookId.isValid() && card.value(QStringLiteral("bookId")) != bookId)
70
1/1
✓ Branch 73 → 74 taken 2 times.
2 return tr("Only contacts of the same address book can be merged. Move them into one book first.");
71
3/5
✓ Branch 79 → 80 taken 38 times.
✓ Branch 80 → 81 taken 38 times.
✗ Branch 85 → 86 not taken.
✓ Branch 85 → 88 taken 38 times.
✗ Branch 86 → 87 not taken.
76 if (!card.value(QStringLiteral("writable")).toBool()) return tr("This address book is read-only.");
72
1/1
✓ Branch 92 → 93 taken 38 times.
76 bookId = card.value(QStringLiteral("bookId"));
73
1/1
✓ Branch 98 → 99 taken 38 times.
38 cards->append(card);
74 41 }
75 13 return {};
76 16 }
77
78 9 QVariantMap QuickContactActions::mergePlan(const QStringList &keys, const QString &keepKey) const {
79 9 QList<QVariantMap> cards;
80
1/1
✓ Branch 2 → 3 taken 9 times.
9 const QString error = mergeCards(keys, &cards);
81
4/6
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 22 taken 7 times.
✓ Branch 15 → 16 taken 4 times.
✓ Branch 15 → 17 taken 2 times.
✗ Branch 371 → 372 not taken.
✗ Branch 371 → 373 not taken.
21 if (!error.isEmpty()) return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), error}};
82
1/1
✓ Branch 22 → 23 taken 7 times.
7 const int keeper = ContactMerge::keeperIndex(cards);
83 // S106-953: the preselected conflict values follow the contact the user
84 // keeps; `keep` stays the proposal (the "most complete" mark).
85
2/2
✓ Branch 24 → 25 taken 3 times.
✓ Branch 24 → 27 taken 4 times.
7 const int chosenKeeper = keys.contains(keepKey) ? int(keys.indexOf(keepKey)) : keeper;
86 7 QVariantList conflicts;
87
5/5
✓ Branch 28 → 29 taken 7 times.
✓ Branch 29 → 30 taken 7 times.
✓ Branch 30 → 31 taken 7 times.
✓ Branch 110 → 32 taken 9 times.
✓ Branch 110 → 111 taken 7 times.
16 for (const QVariant &value : ContactMerge::conflicts(cards, chosenKeeper)) {
88
1/1
✓ Branch 33 → 34 taken 9 times.
9 QVariantMap conflict = value.toMap();
89 9 QVariantList options;
90
6/6
✓ Branch 38 → 39 taken 9 times.
✓ Branch 39 → 40 taken 9 times.
✓ Branch 44 → 45 taken 9 times.
✓ Branch 45 → 46 taken 9 times.
✓ Branch 74 → 47 taken 19 times.
✓ Branch 74 → 75 taken 9 times.
37 for (const QVariant &option : conflict.value(QStringLiteral("options")).toList()) {
91
1/1
✓ Branch 48 → 49 taken 19 times.
19 QVariantMap entry = option.toMap();
92
3/3
✓ Branch 53 → 54 taken 19 times.
✓ Branch 54 → 55 taken 19 times.
✓ Branch 60 → 61 taken 19 times.
57 entry.insert(QStringLiteral("key"), keys.at(entry.value(QStringLiteral("from")).toInt()));
93
1/1
✓ Branch 69 → 70 taken 19 times.
19 options.append(entry);
94 28 }
95
1/1
✓ Branch 80 → 81 taken 9 times.
18 conflict.insert(QStringLiteral("options"), options);
96
3/3
✓ Branch 88 → 89 taken 9 times.
✓ Branch 89 → 90 taken 9 times.
✓ Branch 95 → 96 taken 9 times.
27 conflict.insert(QStringLiteral("chosen"), keys.at(conflict.value(QStringLiteral("chosen")).toInt()));
97
1/1
✓ Branch 104 → 105 taken 9 times.
9 conflicts.append(conflict);
98 16 }
99
1/1
✓ Branch 114 → 115 taken 7 times.
7 const QVariantMap fields = ContactMerge::merged(cards, keeper, {});
100 // Sprint 107 D-16: every list value once, with the cards it comes from.
101 7 QVariantList sets;
102
5/5
✓ Branch 117 → 118 taken 7 times.
✓ Branch 118 → 119 taken 7 times.
✓ Branch 119 → 120 taken 7 times.
✓ Branch 191 → 121 taken 15 times.
✓ Branch 191 → 192 taken 7 times.
22 for (const QVariant &value : ContactMerge::setRows(cards, chosenKeeper)) {
103
1/1
✓ Branch 122 → 123 taken 15 times.
15 QVariantMap row = value.toMap();
104 15 QVariantList values;
105
6/6
✓ Branch 127 → 128 taken 15 times.
✓ Branch 128 → 129 taken 15 times.
✓ Branch 133 → 134 taken 15 times.
✓ Branch 134 → 135 taken 15 times.
✓ Branch 174 → 136 taken 28 times.
✓ Branch 174 → 175 taken 15 times.
58 for (const QVariant &entry : row.value(QStringLiteral("values")).toList()) {
106
1/1
✓ Branch 137 → 138 taken 28 times.
28 QVariantMap mapped = entry.toMap();
107 28 QStringList from;
108
8/8
✓ Branch 142 → 143 taken 28 times.
✓ Branch 143 → 144 taken 28 times.
✓ Branch 148 → 149 taken 28 times.
✓ Branch 149 → 150 taken 28 times.
✓ Branch 152 → 153 taken 37 times.
✓ Branch 154 → 155 taken 37 times.
✓ Branch 157 → 151 taken 37 times.
✓ Branch 157 → 158 taken 28 times.
93 for (const QVariant &index : mapped.value(QStringLiteral("from")).toList()) from.append(keys.at(index.toInt()));
109
1/1
✓ Branch 163 → 164 taken 28 times.
56 mapped.insert(QStringLiteral("from"), from);
110
1/1
✓ Branch 168 → 169 taken 28 times.
28 values.append(mapped);
111 43 }
112
1/1
✓ Branch 180 → 181 taken 15 times.
30 row.insert(QStringLiteral("values"), values);
113
1/1
✓ Branch 185 → 186 taken 15 times.
15 sets.append(row);
114 22 }
115 // Relationships of the merged card (people details travel along, ContactPeopleMerge).
116
2/2
✓ Branch 193 → 194 taken 7 times.
✓ Branch 194 → 195 taken 7 times.
7 QList<ContactCard> parsed = parsedCards(m_coordinator(), keys);
117
1/1
✓ Branch 195 → 196 taken 7 times.
7 const ContactCard kept = parsed.takeAt(keeper);
118 const int relations =
119
5/7
✓ Branch 198 → 199 taken 7 times.
✓ Branch 199 → 200 taken 7 times.
✓ Branch 200 → 201 taken 7 times.
✓ Branch 205 → 206 taken 7 times.
✓ Branch 205 → 207 taken 7 times.
✗ Branch 556 → 557 not taken.
✗ Branch 556 → 558 not taken.
14 ContactPeopleMerge::mergedRelationCount(kept, parsed, ContactPeopleMerge::uidsOf(parsed + QList<ContactCard>{kept}));
120 7 QVariantList names;
121
2/2
✓ Branch 241 → 208 taken 19 times.
✓ Branch 241 → 242 taken 7 times.
26 for (int i = 0; i < cards.size(); ++i)
122
1/1
✓ Branch 225 → 226 taken 19 times.
57 names.append(QVariantMap{{QStringLiteral("key"), keys.at(i)},
123
3/5
✓ Branch 221 → 222 taken 19 times.
✓ Branch 229 → 230 taken 38 times.
✓ Branch 229 → 231 taken 19 times.
✗ Branch 573 → 574 not taken.
✗ Branch 573 → 575 not taken.
114 {QStringLiteral("name"), cards.at(i).value(QStringLiteral("displayName"))}});
124 35 return {{QStringLiteral("ok"), true}, {QStringLiteral("keep"), keys.at(keeper)}, {QStringLiteral("keys"), keys},
125 28 {QStringLiteral("names"), names}, {QStringLiteral("conflicts"), conflicts}, {QStringLiteral("sets"), sets},
126
2/2
✓ Branch 274 → 275 taken 7 times.
✓ Branch 275 → 276 taken 7 times.
21 {QStringLiteral("emails"), fields.value(QStringLiteral("emails")).toList().size()},
127
2/2
✓ Branch 285 → 286 taken 7 times.
✓ Branch 286 → 287 taken 7 times.
21 {QStringLiteral("phones"), fields.value(QStringLiteral("phones")).toList().size()},
128
2/2
✓ Branch 296 → 297 taken 7 times.
✓ Branch 297 → 298 taken 7 times.
21 {QStringLiteral("addresses"), fields.value(QStringLiteral("addresses")).toList().size()},
129 14 {QStringLiteral("relations"), relations},
130
4/6
✓ Branch 311 → 312 taken 7 times.
✓ Branch 312 → 313 taken 7 times.
✓ Branch 316 → 317 taken 77 times.
✓ Branch 316 → 318 taken 7 times.
✗ Branch 609 → 610 not taken.
✗ Branch 609 → 611 not taken.
98 {QStringLiteral("groups"), fields.value(QStringLiteral("categories")).toStringList()}};
131 172 }
132
133 7 QVariantMap QuickContactActions::mergeContacts(const QString &keepKey, const QStringList &keys, const QVariantMap &choices,
134 const QStringList &excluded) {
135 7 QList<QVariantMap> cards;
136
1/1
✓ Branch 2 → 3 taken 7 times.
7 const QString error = mergeCards(keys, &cards);
137
3/3
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 7 taken 6 times.
✓ Branch 5 → 6 taken 1 time.
7 if (!error.isEmpty()) return failure(error);
138 6 const int keeper = int(keys.indexOf(keepKey));
139
1/4
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 14 taken 6 times.
✗ Branch 9 → 10 not taken.
✗ Branch 10 → 11 not taken.
6 if (keeper < 0) return failure(tr("The contact to keep is not among the selected contacts."));
140 6 QMap<QString, int> chosen;
141
4/4
✓ Branch 14 → 15 taken 6 times.
✓ Branch 29 → 30 taken 8 times.
✓ Branch 31 → 16 taken 2 times.
✓ Branch 31 → 32 taken 6 times.
8 for (auto it = choices.begin(); it != choices.end(); ++it)
142
4/5
✓ Branch 17 → 18 taken 2 times.
✓ Branch 20 → 21 taken 2 times.
✗ Branch 20 → 28 not taken.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 25 → 26 taken 2 times.
2 if (keys.contains(it.value().toString())) chosen.insert(it.key(), int(keys.indexOf(it.value().toString())));
143
1/1
✓ Branch 32 → 33 taken 6 times.
6 const QVariantMap fields = ContactMerge::merged(cards, keeper, chosen, excluded);
144 // The people details (relationships, how we met, profiles, …) are merged
145 // on the parsed cards: the field map above does not carry them all.
146
2/2
✓ Branch 33 → 34 taken 6 times.
✓ Branch 34 → 35 taken 6 times.
6 QList<ContactCard> others = parsedCards(m_coordinator(), keys);
147
1/1
✓ Branch 35 → 36 taken 6 times.
6 const ContactCard keptBefore = others.takeAt(keeper);
148
4/6
✓ Branch 38 → 39 taken 6 times.
✓ Branch 39 → 40 taken 6 times.
✓ Branch 43 → 44 taken 6 times.
✓ Branch 43 → 45 taken 6 times.
✗ Branch 271 → 272 not taken.
✗ Branch 271 → 273 not taken.
12 const QSet<QString> mergedUids = ContactPeopleMerge::uidsOf(others + QList<ContactCard>{keptBefore});
149 6 ContactCard keptAfter = keptBefore;
150
1/1
✓ Branch 46 → 47 taken 6 times.
6 ContactPeopleMerge::mergeInto(&keptAfter, others, mergedUids);
151
3/3
✓ Branch 47 → 48 taken 6 times.
✓ Branch 52 → 53 taken 6 times.
✓ Branch 53 → 54 taken 6 times.
12 const bool wasMe = keys.contains(QSettings().value(QStringLiteral("contacts/meCard")).toString());
152 6 QList<UndoPart> parts;
153 6 m_collect = &parts;
154
1/1
✓ Branch 60 → 61 taken 6 times.
6 const QVariantMap saved = save(keepKey, fields, true);
155
2/2
✓ Branch 65 → 66 taken 6 times.
✓ Branch 66 → 67 taken 6 times.
12 bool removed = saved.value(QStringLiteral("ok")).toBool();
156
7/20
✓ Branch 71 → 72 taken 6 times.
✗ Branch 71 → 78 not taken.
✓ Branch 76 → 77 taken 6 times.
✓ Branch 77 → 79 taken 6 times.
✓ Branch 79 → 80 taken 6 times.
✗ Branch 79 → 81 not taken.
✓ Branch 81 → 82 taken 6 times.
✗ Branch 81 → 83 not taken.
✓ Branch 83 → 84 taken 6 times.
✗ Branch 83 → 85 not taken.
✓ Branch 85 → 86 taken 6 times.
✗ Branch 85 → 87 not taken.
✗ Branch 308 → 309 not taken.
✗ Branch 308 → 310 not taken.
✗ Branch 312 → 313 not taken.
✗ Branch 312 → 314 not taken.
✗ Branch 316 → 317 not taken.
✗ Branch 316 → 318 not taken.
✗ Branch 320 → 321 not taken.
✗ Branch 320 → 322 not taken.
12 const QString keptKey = removed ? saved.value(QStringLiteral("key")).toString() : keepKey;
157
6/7
✓ Branch 87 → 88 taken 6 times.
✗ Branch 87 → 91 not taken.
✓ Branch 88 → 89 taken 6 times.
✓ Branch 89 → 90 taken 1 time.
✓ Branch 89 → 91 taken 5 times.
✓ Branch 92 → 93 taken 1 time.
✓ Branch 92 → 134 taken 5 times.
6 if (removed && !(keptAfter == keptBefore)) {
158 1 const QVariantMap people = editPeople(
159 1 keptKey, [&](ContactCard *card) -> QString {
160 1 ContactPeopleMerge::mergeInto(card, others, mergedUids);
161 1 return {};
162 },
163
1/1
✓ Branch 95 → 96 taken 1 time.
3 QString());
164
2/2
✓ Branch 102 → 103 taken 1 time.
✓ Branch 103 → 104 taken 1 time.
2 removed = people.value(QStringLiteral("ok")).toBool();
165
1/2
✗ Branch 108 → 109 not taken.
✓ Branch 108 → 132 taken 1 time.
1 if (!removed)
166 ✗ qCWarning(lcContactActions) << "Merge: people details not saved:" << people.value(QStringLiteral("error")).toString();
167 1 }
168 // S106-299: every duplicate is removed on its own; one that cannot be
169 // removed does not stop the others, and the references of each card that
170 // is gone move to the kept one (also when another removal failed).
171 6 const bool kept = removed;
172 6 QSet<QString> goneUids;
173
1/2
✓ Branch 135 → 136 taken 6 times.
✗ Branch 135 → 162 not taken.
6 if (kept)
174
2/2
✓ Branch 160 → 138 taken 16 times.
✓ Branch 160 → 161 taken 6 times.
22 for (const QString &key : keys) {
175
2/2
✓ Branch 140 → 141 taken 6 times.
✓ Branch 140 → 142 taken 10 times.
16 if (key == keepKey)
176 6 continue;
177 10 qint64 bookId = 0;
178 10 QString uid;
179
7/8
✓ Branch 142 → 143 taken 10 times.
✓ Branch 143 → 144 taken 9 times.
✓ Branch 143 → 147 taken 1 time.
✓ Branch 144 → 145 taken 9 times.
✓ Branch 145 → 146 taken 9 times.
✗ Branch 145 → 147 not taken.
✓ Branch 148 → 149 taken 9 times.
✓ Branch 148 → 155 taken 1 time.
10 if (remove(key) && ContactQueries::splitKey(key, &bookId, &uid))
180
3/3
✓ Branch 149 → 150 taken 9 times.
✓ Branch 150 → 151 taken 9 times.
✓ Branch 151 → 152 taken 9 times.
9 goneUids.insert(uid.trimmed().toLower());
181 else
182 1 removed = false;
183 10 }
184
3/3
✓ Branch 162 → 163 taken 6 times.
✓ Branch 163 → 164 taken 6 times.
✓ Branch 164 → 165 taken 6 times.
6 goneUids.remove(keptBefore.uid.trimmed().toLower());
185 // Other cards that named a removed card now name the kept one.
186
1/2
✓ Branch 168 → 169 taken 6 times.
✗ Branch 168 → 170 not taken.
6 if (!goneUids.isEmpty())
187
1/1
✓ Branch 169 → 170 taken 6 times.
6 retargetReferences(goneUids, keptKey);
188 6 m_collect = nullptr;
189
3/4
✓ Branch 170 → 171 taken 6 times.
✗ Branch 170 → 173 not taken.
✓ Branch 171 → 172 taken 1 time.
✓ Branch 171 → 173 taken 5 times.
6 if (kept && wasMe)
190
1/1
✓ Branch 172 → 173 taken 1 time.
1 setMe(keptKey); // the me card lives on in the merged card (D-6)
191
11/12
✓ Branch 173 → 174 taken 6 times.
✓ Branch 177 → 178 taken 6 times.
✓ Branch 178 → 179 taken 6 times.
✓ Branch 180 → 181 taken 6 times.
✓ Branch 181 → 182 taken 6 times.
✓ Branch 182 → 183 taken 5 times.
✓ Branch 182 → 184 taken 1 time.
✓ Branch 184 → 185 taken 1 time.
✗ Branch 184 → 186 not taken.
✓ Branch 188 → 189 taken 6 times.
✓ Branch 195 → 175 taken 6 times.
✓ Branch 195 → 196 taken 6 times.
12 qCInfo(lcContactActions) << "Merge of" << keys.size() << "cards:" << (removed ? "done" : kept ? "partly done" : "failed")
192
2/2
✓ Branch 190 → 191 taken 6 times.
✓ Branch 191 → 192 taken 6 times.
6 << goneUids.size() << "removed";
193
1/2
✓ Branch 197 → 198 taken 6 times.
✗ Branch 197 → 203 not taken.
6 if (!parts.isEmpty()) {
194 6 m_undo.append({tr("Contacts merged"), parts});
195
1/1
✓ Branch 202 → 203 taken 6 times.
6 emit undoChanged();
196 }
197
2/2
✓ Branch 203 → 204 taken 1 time.
✓ Branch 203 → 212 taken 5 times.
6 if (!removed)
198
4/8
✓ Branch 204 → 205 taken 1 time.
✗ Branch 204 → 207 not taken.
✓ Branch 205 → 206 taken 1 time.
✓ Branch 206 → 208 taken 1 time.
✓ Branch 209 → 210 taken 1 time.
✗ Branch 209 → 211 not taken.
✗ Branch 394 → 395 not taken.
✗ Branch 394 → 396 not taken.
2 return kept ? failure(tr("A merged contact could not be removed.")) : saved;
199
2/2
✓ Branch 213 → 214 taken 5 times.
✓ Branch 214 → 215 taken 5 times.
5 emit done(tr("%n contacts merged", nullptr, int(keys.size())), true);
200
3/5
✓ Branch 227 → 228 taken 5 times.
✓ Branch 231 → 232 taken 10 times.
✓ Branch 231 → 233 taken 5 times.
✗ Branch 402 → 403 not taken.
✗ Branch 402 → 404 not taken.
35 return {{QStringLiteral("ok"), true}, {QStringLiteral("key"), saved.value(QStringLiteral("key"))}};
201 39 }
202
203 6 void QuickContactActions::retargetReferences(const QSet<QString> &goneUids, const QString &keptKey) {
204
1/1
✓ Branch 2 → 3 taken 6 times.
6 ContactSyncCoordinator *sync = m_coordinator();
205 6 qint64 keptBook = 0;
206 6 QString keptUid;
207
5/9
✓ Branch 3 → 4 taken 6 times.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 6 times.
✗ Branch 5 → 8 not taken.
✓ Branch 6 → 7 taken 6 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 6 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 6 times.
6 if (!sync || goneUids.isEmpty() || !ContactQueries::splitKey(keptKey, &keptBook, &keptUid))
208 ✗ return;
209
3/3
✓ Branch 12 → 13 taken 6 times.
✓ Branch 17 → 18 taken 6 times.
✓ Branch 18 → 19 taken 6 times.
12 const QString keptName = personEntry(keptKey).value(QStringLiteral("name")).toString();
210 // Relationships and introducers (card_relations) and group members (card_members) naming a removed card.
211 6 QStringList holders;
212
2/2
✓ Branch 25 → 26 taken 6 times.
✓ Branch 26 → 27 taken 6 times.
6 QSqlQuery q(sync->store()->database());
213
1/1
✓ Branch 31 → 32 taken 6 times.
6 q.prepare(QStringLiteral(
214 "SELECT c.book_id, c.uid FROM cards c JOIN card_relations r ON r.card_id = c.id "
215 "WHERE lower(r.target_uid) = :uid OR lower(r.target_uri) = 'urn:uuid:' || :uid "
216 "UNION SELECT c.book_id, c.uid FROM cards c JOIN card_members m ON m.card_id = c.id "
217 "WHERE lower(m.member) = :uid OR lower(m.member) = 'urn:uuid:' || :uid"));
218
2/2
✓ Branch 82 → 36 taken 9 times.
✓ Branch 82 → 83 taken 6 times.
15 for (const QString &uid : goneUids) {
219
1/1
✓ Branch 42 → 43 taken 9 times.
18 q.bindValue(QStringLiteral(":uid"), uid);
220
2/3
✓ Branch 46 → 47 taken 9 times.
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 59 taken 9 times.
9 if (!q.exec()) {
221 ✗ qCWarning(lcContactActions) << "Merge: reference lookup failed";
222 ✗ continue;
223 ✗ }
224
3/3
✓ Branch 78 → 79 taken 12 times.
✓ Branch 79 → 60 taken 3 times.
✓ Branch 79 → 80 taken 9 times.
12 while (q.next())
225
5/5
✓ Branch 60 → 61 taken 3 times.
✓ Branch 61 → 62 taken 3 times.
✓ Branch 62 → 63 taken 3 times.
✓ Branch 63 → 64 taken 3 times.
✓ Branch 64 → 65 taken 3 times.
3 if (const QString key = ContactQueries::cardKey(q.value(0).toLongLong(), q.value(1).toString());
226
3/6
✓ Branch 69 → 70 taken 3 times.
✗ Branch 69 → 73 not taken.
✓ Branch 71 → 72 taken 3 times.
✗ Branch 71 → 73 not taken.
✓ Branch 74 → 75 taken 3 times.
✗ Branch 74 → 76 not taken.
3 key != keptKey && !holders.contains(key))
227
1/1
✓ Branch 75 → 76 taken 3 times.
3 holders.append(key);
228 }
229 6 int rewritten = 0;
230
2/2
✓ Branch 144 → 86 taken 3 times.
✓ Branch 144 → 145 taken 6 times.
9 for (const QString &holder : std::as_const(holders)) {
231 3 const QVariantMap result = editPeople(
232
1/1
✓ Branch 88 → 89 taken 3 times.
3 holder, [&](ContactCard *card) -> QString {
233
2/4
✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 5 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 3 times.
3 return ContactPeopleMerge::retarget(card, goneUids, keptUid, keptName) ? QString() : QStringLiteral("unchanged");
234 },
235
1/1
✓ Branch 89 → 90 taken 3 times.
9 QString());
236
3/4
✓ Branch 96 → 97 taken 3 times.
✓ Branch 97 → 98 taken 3 times.
✓ Branch 102 → 103 taken 3 times.
✗ Branch 102 → 104 not taken.
6 if (result.value(QStringLiteral("ok")).toBool())
237 3 ++rewritten;
238 ✗ else if (result.value(QStringLiteral("error")).toString() != QLatin1String("unchanged"))
239 ✗ qCWarning(lcContactActions) << "Merge: a reference on another card was not updated:"
240 ✗ << result.value(QStringLiteral("error")).toString();
241 3 }
242
9/9
✓ Branch 145 → 146 taken 6 times.
✓ Branch 149 → 150 taken 6 times.
✓ Branch 150 → 151 taken 6 times.
✓ Branch 151 → 152 taken 6 times.
✓ Branch 152 → 153 taken 6 times.
✓ Branch 154 → 155 taken 6 times.
✓ Branch 155 → 156 taken 6 times.
✓ Branch 159 → 147 taken 6 times.
✓ Branch 159 → 160 taken 6 times.
12 qCInfo(lcContactActions) << "Merge: references re-pointed on" << rewritten << "of" << holders.size() << "cards";
243 6 }
244
245 2 QVariantMap QuickContactActions::merge(const QString &keepKey, const QString &otherKey) {
246
3/5
✓ Branch 7 → 8 taken 2 times.
✓ Branch 11 → 12 taken 4 times.
✓ Branch 11 → 13 taken 2 times.
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 22 not taken.
8 return mergeContacts(keepKey, {keepKey, otherKey}, {});
247 2 }
248