• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IJHack / QtPass / 27660975269

17 Jun 2026 02:06AM UTC coverage: 57.452% (-0.03%) from 57.477%
27660975269

Pull #1551

github

web-flow
Merge 1c61e7f1d into 4da97561f
Pull Request #1551: refactor(pass): inject AppSettings into Pass hierarchy (PR A of #1511)

44 of 84 new or added lines in 4 files covered. (52.38%)

13 existing lines in 6 files now uncovered.

3982 of 6931 relevant lines covered (57.45%)

23.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.72
/src/imitatepass.cpp
1
// SPDX-FileCopyrightText: 2016 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "imitatepass.h"
4
#include "executor.h"
5
#include "util.h"
6
#include <QDirIterator>
7
#include <QElapsedTimer>
8
#include <QPointer>
9
#include <QRegularExpression>
10
#include <QThread>
11
#include <utility>
12

13
#ifdef QT_DEBUG
14
#include "debughelper.h"
15
#endif
16

17
using Enums::CLIPBOARD_ALWAYS;
18
using Enums::CLIPBOARD_NEVER;
19
using Enums::CLIPBOARD_ON_DEMAND;
20
using Enums::GIT_ADD;
21
using Enums::GIT_COMMIT;
22
using Enums::GIT_COPY;
23
using Enums::GIT_INIT;
24
using Enums::GIT_MOVE;
25
using Enums::GIT_PULL;
26
using Enums::GIT_PUSH;
27
using Enums::GIT_RM;
28
using Enums::GPG_GENKEYS;
29
using Enums::INVALID;
30
using Enums::PASS_COPY;
31
using Enums::PASS_GREP;
32
using Enums::PASS_INIT;
33
using Enums::PASS_INSERT;
34
using Enums::PASS_MOVE;
35
using Enums::PASS_OTP_GENERATE;
36
using Enums::PASS_REMOVE;
37
using Enums::PASS_SHOW;
38
using Enums::PROCESS_COUNT;
39

40
/**
41
 * @brief ImitatePass::ImitatePass for situations when pass is not available
42
 * we imitate the behavior of pass https://www.passwordstore.org/
43
 */
44
ImitatePass::ImitatePass() = default;
76✔
45

46
ImitatePass::~ImitatePass() {
40✔
47
  static constexpr int kGrepThreadTimeoutMs = 5000;
48
  for (QThread *t : std::as_const(m_grepThreads))
38✔
49
    if (t && t->isRunning())
×
50
      t->requestInterruption();
×
51
  QElapsedTimer elapsed;
38✔
52
  elapsed.start();
38✔
53
  for (QThread *t : std::as_const(m_grepThreads)) {
38✔
54
    if (t && t->isRunning()) {
×
55
      const int remaining =
56
          kGrepThreadTimeoutMs - static_cast<int>(elapsed.elapsed());
×
57
      if (remaining > 0)
×
58
        t->wait(remaining);
×
59
    }
60
  }
61
}
40✔
62

63
auto ImitatePass::pgit(const QString &path) const -> QString {
2✔
64
  QString normalizedPath = QDir::cleanPath(path);
2✔
65
  if (!m_settings.gitExecutable.startsWith(QStringLiteral("wsl ")))
4✔
66
    return normalizedPath;
NEW
67
  QString res = QStringLiteral("$(wslpath ") + normalizedPath + QLatin1Char(')');
×
UNCOV
68
  return res.replace('\\', '/');
×
69
}
70

71
auto ImitatePass::pgpg(const QString &path) const -> QString {
37✔
72
  QString normalizedPath = QDir::cleanPath(path);
37✔
73
  if (!m_settings.gpgExecutable.startsWith(QStringLiteral("wsl ")))
74✔
74
    return normalizedPath;
NEW
75
  QString res = QStringLiteral("$(wslpath ") + normalizedPath + QLatin1Char(')');
×
UNCOV
76
  return res.replace('\\', '/');
×
77
}
78

79
/**
80
 * @brief ImitatePass::GitInit git init wrapper
81
 */
82
void ImitatePass::GitInit() {
×
NEW
83
  executeGit(GIT_INIT, {"init", pgit(m_settings.passStore)});
×
84
}
×
85

86
/**
87
 * @brief ImitatePass::GitPull git pull wrapper
88
 */
89
void ImitatePass::GitPull() { executeGit(GIT_PULL, {"pull"}); }
×
90

91
/**
92
 * @brief ImitatePass::GitPull_b git pull wrapper
93
 */
94
void ImitatePass::GitPull_b() {
×
NEW
95
  Executor::executeBlocking(m_settings.gitExecutable, {"pull"});
×
96
}
×
97

98
/**
99
 * @brief ImitatePass::GitPush git push wrapper
100
 */
101
void ImitatePass::GitPush() {
×
NEW
102
  if (m_settings.useGit) {
×
103
    executeGit(GIT_PUSH, {"push"});
×
104
  }
105
}
×
106

107
/**
108
 * @brief ImitatePass::Show shows content of file
109
 */
110
void ImitatePass::Show(QString file) {
10✔
111
  file = m_settings.passStore + file + ".gpg";
10✔
112
  QStringList args = {"-d",      "--quiet",     "--yes",   "--no-encrypt-to",
113
                      "--batch", "--use-agent", pgpg(file)};
80✔
114
  executeGpg(PASS_SHOW, args);
20✔
115
}
20✔
116

117
/**
118
 * @brief ImitatePass::OtpGenerate generates an otp code
119
 */
120
void ImitatePass::OtpGenerate(QString file) {
×
121
#ifdef QT_DEBUG
122
  dbg() << "No OTP generation code for fake pass yet, attempting for file: " +
123
               file;
124
#else
125
  Q_UNUSED(file)
126
#endif
127
}
×
128

129
/**
130
 * @brief ImitatePass::Insert create new file with encrypted content
131
 *
132
 * @param file      file to be created
133
 * @param newValue  value to be stored in file
134
 * @param overwrite whether to overwrite existing file
135
 */
136
void ImitatePass::Insert(QString file, QString newValue, bool overwrite) {
19✔
137
  file = file + ".gpg";
19✔
138
  QString gpgIdPath = Pass::getGpgIdPath(file);
19✔
139
  if (!verifyGpgIdFile(gpgIdPath)) {
19✔
140
    emit critical(tr("Check .gpg-id file signature!"),
×
141
                  tr("Signature for %1 is invalid.").arg(gpgIdPath));
×
142
    return;
×
143
  }
144
  transactionHelper trans(this, PASS_INSERT);
19✔
145
  QStringList recipients = Pass::getRecipientList(file);
19✔
146
  if (recipients.isEmpty()) {
19✔
147
    // Already emit critical signal to notify user of error - no need to throw
148
    emit critical(tr("Can not edit"),
×
149
                  tr("Could not read encryption key to use, .gpg-id "
×
150
                     "file missing or invalid."));
151
    return;
152
  }
153
  QStringList args = {"--batch", "--status-fd", "2",
154
                      "-eq",     "--output",    pgpg(file)};
133✔
155
  for (auto &r : recipients) {
38✔
156
    args.append("-r");
38✔
157
    args.append(r);
158
  }
159
  if (overwrite) {
19✔
160
    args.append("--yes");
2✔
161
  }
162
  args.append("-");
38✔
163
  executeGpg(PASS_INSERT, args, newValue);
19✔
164
  if (!m_settings.useWebDav && m_settings.useGit) {
19✔
165
    // Git is used when enabled - this is the standard pass workflow
166
    if (!overwrite) {
1✔
167
      executeGit(GIT_ADD, {"add", pgit(file)});
4✔
168
    }
169
    QString path = QDir(m_settings.passStore).relativeFilePath(file);
1✔
170
    path.replace(Util::endsWithGpg(), "");
1✔
171
    QString msg =
172
        QString(overwrite ? "Edit" : "Add") + " for " + path + " using QtPass.";
2✔
173
    gitCommit(file, msg);
1✔
174
  }
175
}
20✔
176

177
/**
178
 * @brief ImitatePass::gitCommit commit a file to git with an appropriate commit
179
 * message
180
 * @param file
181
 * @param msg
182
 */
183
void ImitatePass::gitCommit(const QString &file, const QString &msg) {
1✔
184
  if (file.isEmpty()) {
1✔
185
    executeGit(GIT_COMMIT, {"commit", "-m", msg});
×
186
  } else {
187
    executeGit(GIT_COMMIT, {"commit", "-m", msg, "--", pgit(file)});
7✔
188
  }
189
}
3✔
190

191
/**
192
 * @brief ImitatePass::Remove custom implementation of "pass remove"
193
 */
194
void ImitatePass::Remove(QString file, bool isDir) {
1✔
195
  file = m_settings.passStore + file;
1✔
196
  transactionHelper trans(this, PASS_REMOVE);
1✔
197
  if (!isDir) {
1✔
198
    file += ".gpg";
1✔
199
  }
200
  if (m_settings.useGit) {
1✔
201
    executeGit(GIT_RM, {"rm", (isDir ? "-rf" : "-f"), pgit(file)});
×
202
    // Normalize path the same way as add/edit operations
NEW
203
    QString path = QDir(m_settings.passStore).relativeFilePath(file);
×
204
    path.replace(Util::endsWithGpg(), "");
×
205
    gitCommit(file, "Remove for " + path + " using QtPass.");
×
206
  } else {
207
    if (isDir) {
1✔
208
      QDir dir(file);
×
209
      dir.removeRecursively();
×
210
    } else {
×
211
      QFile(file).remove();
1✔
212
    }
213
  }
214
}
1✔
215

216
/**
217
 * @brief ImitatePass::Init initialize pass repository
218
 *
219
 * @param path      path in which new password-store will be created
220
 * @param users     list of users who shall be able to decrypt passwords in
221
 * path
222
 */
223
auto ImitatePass::checkSigningKeys(const QStringList &signingKeys) -> bool {
×
224
  QString out;
×
225
  QStringList args =
226
      QStringList{"--status-fd=1", "--list-secret-keys"} + signingKeys;
×
NEW
227
  int result = Executor::executeBlocking(m_settings.gpgExecutable, args, &out);
×
228
  if (result != 0) {
×
229
#ifdef QT_DEBUG
230
    dbg() << "GPG list-secret-keys failed with code:" << result;
231
#endif
232
    return false;
233
  }
234
  for (auto &key : signingKeys) {
×
235
    if (out.contains("[GNUPG:] KEY_CONSIDERED " + key)) {
×
236
      return true;
237
    }
238
  }
239
  return false;
240
}
×
241

242
/**
243
 * @brief Writes the selected users' GPG key IDs to a .gpg-id file.
244
 * @details Opens the specified file for writing, stores the key ID of each
245
 * enabled user on a separate line, and warns if none of the selected users has
246
 * a secret key available.
247
 *
248
 * @param QString &gpgIdFile - Path to the .gpg-id file to be written.
249
 * @param QList<UserInfo> &users - List of users to evaluate and write to the
250
 * file.
251
 * @return void - This function does not return a value.
252
 *
253
 */
254
void ImitatePass::writeGpgIdFile(const QString &gpgIdFile,
1✔
255
                                 const QList<UserInfo> &users) {
256
  QFile gpgId(gpgIdFile);
1✔
257
  if (!gpgId.open(QIODevice::WriteOnly | QIODevice::Text)) {
1✔
258
    emit critical(tr("Cannot update"),
×
259
                  tr("Failed to open .gpg-id for writing."));
×
260
    return;
261
  }
262
  bool secret_selected = false;
263
  for (const UserInfo &user : users) {
2✔
264
    if (user.enabled) {
1✔
265
      gpgId.write((user.key_id + "\n").toUtf8());
2✔
266
      secret_selected |= user.have_secret;
1✔
267
    }
268
  }
269
  gpgId.close();
1✔
270
  // Lock the file to owner-only access. The .gpg-id leaks which keys the
271
  // store is encrypted to; while the typical ~/.password-store is 0700,
272
  // users may relocate the store onto NFS/SMB/USB where the parent dir
273
  // perms are more lax. On platforms where setPermissions is a no-op
274
  // (Windows), this is silently best-effort.
275
  QFile::setPermissions(gpgIdFile, QFile::ReadOwner | QFile::WriteOwner);
1✔
276
  if (!secret_selected) {
1✔
277
    emit critical(
×
278
        tr("Check selected users!"),
×
279
        tr("None of the selected keys have a secret key available.\n"
×
280
           "You will not be able to decrypt any newly added passwords!"));
281
  }
282
}
1✔
283

284
/**
285
 * @brief Signs a GPG ID file and verifies its signature.
286
 * @example
287
 * bool result = ImitatePass::signGpgIdFile(gpgIdFile, signingKeys);
288
 * std::cout << result << std::endl; // Expected output: true if signing and
289
 * verification succeed
290
 *
291
 * @param QString &gpgIdFile - Path to the .gpg-id file to be signed.
292
 * @param QStringList &signingKeys - List of signing keys; only the first key is
293
 * used.
294
 * @return bool - True if the file was signed and its signature verified
295
 * successfully; otherwise false.
296
 */
297
auto ImitatePass::signGpgIdFile(const QString &gpgIdFile,
×
298
                                const QStringList &signingKeys) -> bool {
299
  QStringList args;
×
300
  // Use only the first signing key; multiple --default-key options would
301
  // override each other and only the last one would take effect.
302
  if (!signingKeys.isEmpty()) {
×
303
#ifdef QT_DEBUG
304
    if (signingKeys.size() > 1) {
305
      dbg() << "Multiple signing keys configured; using only the first key:"
306
            << signingKeys.first();
307
    }
308
#endif
309
    args.append(QStringList{"--default-key", signingKeys.first()});
×
310
  }
311
  args.append(QStringList{"--yes", "--detach-sign", gpgIdFile});
×
NEW
312
  int result = Executor::executeBlocking(m_settings.gpgExecutable, args);
×
313
  if (result != 0) {
×
314
#ifdef QT_DEBUG
315
    dbg() << "GPG signing failed with code:" << result;
316
#endif
317
    emit critical(tr("GPG signing failed!"),
×
318
                  tr("Failed to sign %1.").arg(gpgIdFile));
×
319
    return false;
×
320
  }
321
  if (!verifyGpgIdFile(gpgIdFile)) {
×
322
    emit critical(tr("Check .gpg-id file signature!"),
×
323
                  tr("Signature for %1 is invalid.").arg(gpgIdFile));
×
324
    return false;
×
325
  }
326
  return true;
327
}
×
328

329
/**
330
 * @brief Adds a GPG ID file and optionally its signature file to git, then
331
 * creates corresponding commit(s).
332
 * @example
333
 * void result = ImitatePass::gitAddGpgId(gpgIdFile, gpgIdSigFile, true, true);
334
 *
335
 * @param const QString &gpgIdFile - Path to the GPG ID file to add and commit.
336
 * @param const QString &gpgIdSigFile - Path to the signature file associated
337
 * with the GPG ID file.
338
 * @param bool addFile - Whether to stage and commit the GPG ID file.
339
 * @param bool addSigFile - Whether to stage and commit the signature file.
340
 * @return void - This function does not return a value.
341
 */
342
void ImitatePass::gitAddGpgId(const QString &gpgIdFile,
×
343
                              const QString &gpgIdSigFile, bool addFile,
344
                              bool addSigFile) {
345
  if (addFile) {
×
346
    executeGit(GIT_ADD, {"add", pgit(gpgIdFile)});
×
347
  }
348
  QString commitPath = gpgIdFile;
349
  commitPath.replace(Util::endsWithGpg(), "");
×
350
  gitCommit(gpgIdFile, "Added " + commitPath + " using QtPass.");
×
351
  if (!addSigFile) {
×
352
    return;
353
  }
354
  executeGit(GIT_ADD, {"add", pgit(gpgIdSigFile)});
×
355
  commitPath = gpgIdSigFile;
×
356
  commitPath.replace(QRegularExpression("\\.gpg$"), "");
×
357
  gitCommit(gpgIdSigFile, "Added " + commitPath + " using QtPass.");
×
358
}
×
359

360
/**
361
 * @brief Initializes the pass entry by writing and optionally signing the GPG
362
 * ID files.
363
 *
364
 * @example
365
 * void result = ImitatePass::Init(path, users);
366
 *
367
 * @param QString path - Base path for the pass entry where ".gpg-id" and
368
 * optional signature files are created.
369
 * @param const QList<UserInfo> &users - List of users whose keys are written
370
 * into the GPG ID file.
371
 * @return void - No return value.
372
 */
373
void ImitatePass::Init(QString path, const QList<UserInfo> &users) {
×
374
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
375
  QStringList signingKeys =
NEW
376
      m_settings.passSigningKey.split(" ", Qt::SkipEmptyParts);
×
377
#else
378
  QStringList signingKeys =
379
      m_settings.passSigningKey.split(" ", QString::SkipEmptyParts);
380
#endif
381
  QString gpgIdSigFile = path + ".gpg-id.sig";
×
382
  bool addSigFile = false;
383
  if (!signingKeys.isEmpty()) {
×
384
    if (!checkSigningKeys(signingKeys)) {
×
385
      emit critical(tr("No signing key!"),
×
386
                    tr("None of the secret signing keys is available.\n"
×
387
                       "You will not be able to change the user list!"));
388
      return;
×
389
    }
390
    QFileInfo checkFile(gpgIdSigFile);
×
391
    if (!checkFile.exists() || !checkFile.isFile()) {
×
392
      addSigFile = true;
393
    }
394
  }
×
395

396
  QString gpgIdFile = path + ".gpg-id";
×
397
  bool addFile = false;
398
  transactionHelper trans(this, PASS_INIT);
×
NEW
399
  if (m_settings.addGPGId) {
×
400
    QFileInfo checkFile(gpgIdFile);
×
401
    if (!checkFile.exists() || !checkFile.isFile()) {
×
402
      addFile = true;
403
    }
404
  }
×
405
  writeGpgIdFile(gpgIdFile, users);
×
406

407
  if (!signingKeys.isEmpty()) {
×
408
    if (!signGpgIdFile(gpgIdFile, signingKeys)) {
×
409
      return;
410
    }
411
  }
412

NEW
413
  if (!m_settings.useWebDav && m_settings.useGit &&
×
414
      !m_settings.gitExecutable.isEmpty()) {
UNCOV
415
    gitAddGpgId(gpgIdFile, gpgIdSigFile, addFile, addSigFile);
×
416
  }
417
  reencryptPath(path);
×
418
}
419

420
/**
421
 * @brief ImitatePass::verifyGpgIdFile verify detached gpgid file signature.
422
 * @param file which gpgid file.
423
 * @return was verification successful?
424
 */
425
auto ImitatePass::verifyGpgIdFile(const QString &file) -> bool {
20✔
426
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
427
  QStringList signingKeys =
428
      m_settings.passSigningKey.split(" ", Qt::SkipEmptyParts);
40✔
429
#else
430
  QStringList signingKeys =
431
      m_settings.passSigningKey.split(" ", QString::SkipEmptyParts);
432
#endif
433
  if (signingKeys.isEmpty()) {
20✔
434
    return true;
435
  }
436
  QString out;
×
437
  QStringList args =
438
      QStringList{"--verify", "--status-fd=1", pgpg(file) + ".sig", pgpg(file)};
×
NEW
439
  int result = Executor::executeBlocking(m_settings.gpgExecutable, args, &out);
×
440
  if (result != 0) {
×
441
#ifdef QT_DEBUG
442
    dbg() << "GPG verify failed with code:" << result;
443
#endif
444
    return false;
445
  }
446
  QRegularExpression re(
447
      R"(^\[GNUPG:\] VALIDSIG ([A-F0-9]{40}) .* ([A-F0-9]{40})\r?$)",
448
      QRegularExpression::MultilineOption);
×
449
  QRegularExpressionMatch m = re.match(out);
×
450
  if (!m.hasMatch()) {
×
451
    return false;
452
  }
453
  QStringList fingerprints = m.capturedTexts();
×
454
  fingerprints.removeFirst();
×
455
  for (auto &key : signingKeys) {
×
456
    if (fingerprints.contains(key)) {
×
457
      return true;
×
458
    }
459
  }
460
  return false;
×
461
}
×
462

463
/**
464
 * @brief ImitatePass::removeDir delete folder recursive.
465
 * @param dirName which folder.
466
 * @return was removal successful?
467
 */
468
auto ImitatePass::removeDir(const QString &dirName) -> bool {
1✔
469
  bool result = true;
470
  QDir dir(dirName);
1✔
471

472
  if (dir.exists(dirName)) {
1✔
473
    for (const QFileInfo &info :
474
         dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden |
475
                               QDir::AllDirs | QDir::Files,
476
                           QDir::DirsFirst)) {
1✔
477
      if (info.isDir()) {
×
478
        result = removeDir(info.absoluteFilePath());
×
479
      } else {
480
        result = QFile::remove(info.absoluteFilePath());
×
481
      }
482

483
      if (!result) {
×
484
        return result;
485
      }
486
    }
487
    result = dir.rmdir(dirName);
1✔
488
  }
489
  return result;
490
}
1✔
491

492
/**
493
 * @brief ImitatePass::reencryptPath reencrypt all files under the chosen
494
 * directory
495
 *
496
 * This is still quite experimental..
497
 * @param dir
498
 */
499
auto ImitatePass::verifyGpgIdForDir(const QString &file,
2✔
500
                                    QStringList &gpgIdFilesVerified,
501
                                    QStringList &gpgId) -> bool {
502
  QString gpgIdPath = Pass::getGpgIdPath(file);
2✔
503
  if (gpgIdFilesVerified.contains(gpgIdPath)) {
2✔
504
    return true;
505
  }
506
  if (!verifyGpgIdFile(gpgIdPath)) {
1✔
507
    emit critical(tr("Check .gpg-id file signature!"),
×
508
                  tr("Signature for %1 is invalid.").arg(gpgIdPath));
×
509
    return false;
×
510
  }
511
  gpgIdFilesVerified.append(gpgIdPath);
512
  gpgId = getRecipientList(file);
2✔
513
  gpgId.sort();
514
  return true;
515
}
516

517
/**
518
 * @brief Extracts and returns a sorted list of valid key IDs from a GPG key
519
 * listing file.
520
 * @example
521
 * QStringList result = ImitatePass::getKeysFromFile(fileName);
522
 * std::cout << result.join(", ").toStdString() << std::endl;
523
 *
524
 * @param fileName - Path to the file used to query and parse GPG key
525
 * information.
526
 * @return QStringList - A sorted list of 16-character key IDs found in the
527
 * file.
528
 */
529
auto ImitatePass::getKeysFromFile(const QString &fileName) -> QStringList {
2✔
530
  QStringList args = {
531
      "-v",          "--no-secmem-warning", "--no-permission-warning",
532
      "--list-only", "--keyid-format=long", pgpg(fileName)};
14✔
533
  QString keys;
2✔
534
  QString err;
2✔
535
  const int result =
536
      Executor::executeBlocking(m_settings.gpgExecutable, args, &keys, &err);
2✔
537
  if (result != 0 && keys.isEmpty() && err.isEmpty()) {
2✔
538
    return {};
×
539
  }
540
  QStringList actualKeys;
2✔
541
  keys += err;
542
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
543
  QStringList key = keys.split(Util::newLinesRegex(), Qt::SkipEmptyParts);
2✔
544
#else
545
  QStringList key = keys.split(Util::newLinesRegex(), QString::SkipEmptyParts);
546
#endif
547
  QListIterator<QString> itr(key);
548
  while (itr.hasNext()) {
8✔
549
    QString current = itr.next();
550
    QStringList cur = current.split(" ");
12✔
551
    if (cur.length() > 4) {
6✔
552
      QString actualKey = cur.takeAt(4);
4✔
553
      if (actualKey.length() == 16) {
4✔
554
        actualKeys << actualKey;
555
      }
556
    }
557
  }
558
  actualKeys.sort();
559
  return actualKeys;
560
}
2✔
561

562
/**
563
 * @brief Re-encrypts a single encrypted file for a new set of recipients.
564
 * @example
565
 * bool result = ImitatePass::reencryptSingleFile(fileName, recipients);
566
 * std::cout << result << std::endl; // Expected output: true on success, false
567
 * on failure
568
 *
569
 * @param const QString &fileName - Path to the encrypted file to re-encrypt.
570
 * @param const QStringList &recipients - List of recipient keys to encrypt the
571
 * file to.
572
 * @return bool - True if the file was successfully decrypted, re-encrypted,
573
 * verified, and replaced; otherwise false.
574
 */
575
auto ImitatePass::reencryptSingleFile(const QString &fileName,
2✔
576
                                      const QStringList &recipients) -> bool {
577
#ifdef QT_DEBUG
578
  dbg() << "reencrypt " << fileName << " for " << recipients;
579
#endif
580
  QString local_lastDecrypt;
2✔
581
  QStringList args = {
582
      "-d",      "--quiet",     "--yes",       "--no-encrypt-to",
583
      "--batch", "--use-agent", pgpg(fileName)};
16✔
584
  int result = Executor::executeBlocking(m_settings.gpgExecutable, args,
2✔
585
                                         &local_lastDecrypt);
586

587
  if (result != 0 || local_lastDecrypt.isEmpty()) {
2✔
588
#ifdef QT_DEBUG
589
    dbg() << "Decrypt error on re-encrypt for:" << fileName;
590
#endif
591
    return false;
592
  }
593

594
  if (local_lastDecrypt.right(1) != "\n") {
4✔
595
    local_lastDecrypt += "\n";
×
596
  }
597

598
  // Use passed recipients instead of re-reading from file
599
  if (recipients.isEmpty()) {
2✔
600
    emit critical(tr("Can not edit"),
×
601
                  tr("Could not read encryption key to use, .gpg-id "
×
602
                     "file missing or invalid."));
603
    return false;
×
604
  }
605

606
  // Encrypt to temporary file for atomic replacement
607
  QString tempPath = fileName + ".reencrypt.tmp";
2✔
608
  args = QStringList{"--yes", "--batch", "-eq", "--output", pgpg(tempPath)};
14✔
609
  for (const auto &i : recipients) {
4✔
610
    args.append("-r");
4✔
611
    args.append(i);
612
  }
613
  args.append("-");
2✔
614
  result = Executor::executeBlocking(m_settings.gpgExecutable, args,
2✔
615
                                     local_lastDecrypt);
616

617
  if (result != 0) {
2✔
618
#ifdef QT_DEBUG
619
    dbg() << "Encrypt error on re-encrypt for:" << fileName;
620
#endif
621
    QFile::remove(tempPath);
×
622
    return false;
623
  }
624

625
  // Verify encryption worked by attempting to decrypt the temp file
626
  QString verifyOutput;
2✔
627
  args = QStringList{"-d", "--quiet", "--batch", "--use-agent", pgpg(tempPath)};
14✔
628
  result =
629
      Executor::executeBlocking(m_settings.gpgExecutable, args, &verifyOutput);
2✔
630
  if (result != 0 || verifyOutput.isEmpty()) {
2✔
631
#ifdef QT_DEBUG
632
    dbg() << "Verification failed for:" << tempPath;
633
#endif
634
    QFile::remove(tempPath);
×
635
    return false;
636
  }
637
  // Verify content matches original decrypted content (defense in depth)
638
  if (verifyOutput.trimmed() != local_lastDecrypt.trimmed()) {
2✔
639
#ifdef QT_DEBUG
640
    dbg() << "Verification content mismatch for:" << tempPath;
641
#endif
642
    QFile::remove(tempPath);
×
643
    return false;
644
  }
645

646
  // Atomic replace with backup: rename original to .bak, rename temp to
647
  // original, then remove backup
648
  QString backupPath = fileName + ".reencrypt.bak";
2✔
649
  if (!QFile::rename(fileName, backupPath)) {
2✔
650
#ifdef QT_DEBUG
651
    dbg() << "Failed to backup original file:" << fileName;
652
#endif
653
    QFile::remove(tempPath);
×
654
    return false;
655
  }
656
  if (!QFile::rename(tempPath, fileName)) {
2✔
657
#ifdef QT_DEBUG
658
    dbg() << "Failed to rename temp file to:" << fileName;
659
#endif
660
    // Restore backup and clean up temp file
661
    QFile::rename(backupPath, fileName);
×
662
    QFile::remove(tempPath);
×
663
    emit critical(
×
664
        tr("Re-encryption failed"),
×
665
        tr("Failed to replace %1. Original has been restored.").arg(fileName));
×
666
    return false;
×
667
  }
668
  // Success - remove backup
669
  QFile::remove(backupPath);
2✔
670

671
  if (!m_settings.useWebDav && m_settings.useGit) {
2✔
NEW
672
    Executor::executeBlocking(m_settings.gitExecutable,
×
673
                              {"add", pgit(fileName)});
NEW
674
    QString path = QDir(m_settings.passStore).relativeFilePath(fileName);
×
675
    path.replace(Util::endsWithGpg(), "");
×
NEW
676
    Executor::executeBlocking(m_settings.gitExecutable,
×
677
                              {"commit", pgit(fileName), "-m",
678
                               "Re-encrypt for " + path + " using QtPass."});
×
679
  }
680

681
  return true;
682
}
6✔
683

684
/**
685
 * @brief Create git backup commit before re-encryption.
686
 * @return true if backup created or not needed, false if backup failed.
687
 */
688
auto ImitatePass::createBackupCommit() -> bool {
1✔
689
  if (!m_settings.useGit || m_settings.gitExecutable.isEmpty()) {
1✔
690
    return true;
691
  }
692
  emit statusMsg(tr("Creating backup commit"), 2000);
×
693
  const QString git = m_settings.gitExecutable;
694
  QString statusOut;
×
695
  if (Executor::executeBlocking(git, {"status", "--porcelain"}, &statusOut) !=
×
696
      0) {
697
    emit critical(
×
698
        tr("Backup commit failed"),
×
699
        tr("Could not inspect git status. Re-encryption was aborted."));
×
700
    return false;
×
701
  }
702
  if (!statusOut.trimmed().isEmpty()) {
×
703
    if (Executor::executeBlocking(git, {"add", "-A"}) != 0 ||
×
704
        Executor::executeBlocking(
×
705
            git, {"commit", "-m", "Backup before re-encryption"}) != 0) {
706
      emit critical(tr("Backup commit failed"),
×
707
                    tr("Re-encryption was aborted because a git backup could "
×
708
                       "not be created."));
709
      return false;
×
710
    }
711
  }
712
  return true;
713
}
×
714

715
/**
716
 * @brief Re-encrypts all `.gpg` files under the given directory using the
717
 *        verified GPG key configuration for each folder.
718
 *
719
 * This method optionally pulls the latest changes before starting, creates a
720
 * backup commit, verifies `.gpg-id` files per directory, and re-encrypts files
721
 * whose current recipients do not match the expected keys. It emits progress,
722
 * status, and error signals throughout the process, and optionally pushes the
723
 * updated password-store when finished.
724
 *
725
 * @param dir - Root directory to scan recursively for `.gpg` files.
726
 * @return void
727
 */
728
void ImitatePass::reencryptPath(const QString &dir) {
1✔
729
  emit statusMsg(tr("Re-encrypting from folder %1").arg(dir), 3000);
2✔
730
  emit startReencryptPath();
1✔
731
  if (m_settings.autoPull && m_settings.useGit) {
1✔
732
    emit statusMsg(tr("Updating password-store"), 2000);
×
733
    GitPull_b();
×
734
  }
735

736
  // Create backup before re-encryption - abort if it fails
737
  if (!createBackupCommit()) {
1✔
738
    emit endReencryptPath();
×
739
    return;
×
740
  }
741

742
  QDir currentDir;
2✔
743
  QDirIterator gpgFiles(dir, QStringList() << "*.gpg", QDir::Files,
2✔
744
                        QDirIterator::Subdirectories);
1✔
745
  QStringList gpgIdFilesVerified;
1✔
746
  QStringList gpgId;
1✔
747
  int successCount = 0;
748
  int failCount = 0;
749
  while (gpgFiles.hasNext()) {
3✔
750
    QString fileName = gpgFiles.next();
2✔
751
    if (gpgFiles.fileInfo().path() != currentDir.path()) {
4✔
752
      if (!verifyGpgIdForDir(fileName, gpgIdFilesVerified, gpgId)) {
2✔
753
        emit endReencryptPath();
×
754
        return;
755
      }
756
      if (gpgId.isEmpty() && !gpgIdFilesVerified.isEmpty()) {
2✔
757
        emit critical(tr("GPG ID verification failed"),
×
758
                      tr("Could not verify .gpg-id for directory."));
×
759
        emit endReencryptPath();
×
760
        return;
761
      }
762
    }
763
    QStringList actualKeys = getKeysFromFile(fileName);
2✔
764
    if (actualKeys != gpgId) {
2✔
765
      if (reencryptSingleFile(fileName, gpgId)) {
2✔
766
        successCount++;
2✔
767
      } else {
768
        failCount++;
×
769
        emit critical(tr("Re-encryption failed"),
×
770
                      tr("Failed to re-encrypt %1").arg(fileName));
×
771
      }
772
    }
773
  }
774

775
  if (failCount > 0) {
1✔
776
    emit statusMsg(tr("Re-encryption completed: %1 succeeded, %2 failed")
×
777
                       .arg(successCount)
×
778
                       .arg(failCount),
×
779
                   5000);
780
  } else {
781
    emit statusMsg(
1✔
782
        tr("Re-encryption completed: %1 files re-encrypted").arg(successCount),
2✔
783
        3000);
784
  }
785

786
  if (m_settings.autoPush && m_settings.useGit) {
1✔
787
    emit statusMsg(tr("Updating password-store"), 2000);
×
788
    GitPush();
×
789
  }
790
  emit endReencryptPath();
1✔
791
}
1✔
792

793
/**
794
 * @brief Resolves the final destination path for moving a file or directory,
795
 * applying .gpg handling for files.
796
 * @example
797
 * QString result = ImitatePass::resolveMoveDestination("/tmp/source.txt",
798
 * "/backup", false); std::cout << result.toStdString() << std::endl; //
799
 * Expected output sample: "/backup/source.txt.gpg"
800
 *
801
 * @param src - Source path to the file or directory.
802
 * @param dest - Requested destination path, which may be a file or directory.
803
 * @param force - When true, allows overwriting an existing destination file.
804
 * @return QString - Resolved destination path, or an empty QString if the
805
 * source/destination is invalid or conflicts occur.
806
 */
807
auto ImitatePass::resolveMoveDestination(const QString &src,
6✔
808
                                         const QString &dest, bool force)
809
    -> QString {
810
  QFileInfo srcFileInfo(src);
6✔
811
  QFileInfo destFileInfo(dest);
6✔
812
  QString destFile;
6✔
813
  QString srcFileBaseName = srcFileInfo.fileName();
6✔
814

815
  if (srcFileInfo.isFile()) {
6✔
816
    if (destFileInfo.isFile()) {
5✔
817
      if (!force) {
2✔
818
#ifdef QT_DEBUG
819
        dbg() << "Destination file already exists";
820
#endif
821
        return {};
822
      }
823
      destFile = dest;
1✔
824
    } else if (destFileInfo.isDir()) {
3✔
825
      destFile = QDir(dest).filePath(srcFileBaseName);
2✔
826
    } else {
827
      destFile = dest;
2✔
828
    }
829

830
    if (destFile.endsWith(".gpg", Qt::CaseInsensitive)) {
8✔
831
      destFile.chop(4);
4✔
832
    }
833
    destFile.append(".gpg");
4✔
834
  } else if (srcFileInfo.isDir()) {
1✔
835
    if (destFileInfo.isDir()) {
×
836
      destFile = QDir(dest).filePath(srcFileBaseName);
×
837
    } else if (destFileInfo.isFile()) {
×
838
#ifdef QT_DEBUG
839
      dbg() << "Destination is a file";
840
#endif
841
      return {};
842
    } else {
843
      destFile = dest;
×
844
    }
845
  } else {
846
#ifdef QT_DEBUG
847
    dbg() << "Source file does not exist";
848
#endif
849
    return {};
850
  }
851
  return destFile;
852
}
6✔
853

854
/**
855
 * @brief Moves a password store item in the Git repository and commits the
856
 * change.
857
 * @example
858
 * void result = className.executeMoveGit(src, destFile, force);
859
 *
860
 * @param const QString &src - Source path of the item to move.
861
 * @param const QString &destFile - Destination path of the item after the move.
862
 * @param bool force - Whether to force the move using Git's -f option.
863
 * @return void - This method does not return a value.
864
 */
865
void ImitatePass::executeMoveGit(const QString &src, const QString &destFile,
×
866
                                 bool force) {
867
  QStringList args;
×
868
  args << "mv";
×
869
  if (force) {
×
870
    args << "-f";
×
871
  }
872
  args << pgit(src);
×
873
  args << pgit(destFile);
×
874
  executeGit(GIT_MOVE, args);
×
875

NEW
876
  QString relSrc = QDir(m_settings.passStore).relativeFilePath(src);
×
877
  relSrc.replace(Util::endsWithGpg(), "");
×
NEW
878
  QString relDest = QDir(m_settings.passStore).relativeFilePath(destFile);
×
879
  relDest.replace(Util::endsWithGpg(), "");
×
880
  QString message = QString("Moved for %1 to %2 using QtPass.");
×
881
  message = message.arg(relSrc, relDest);
×
882
  gitCommit("", message);
×
883
}
×
884

885
/**
886
 * @brief Moves a password entry from the source path to the destination path.
887
 * @example
888
 * ImitatePass::Move(src, dest, true);
889
 *
890
 * @param const QString src - The source path or entry name to move.
891
 * @param const QString dest - The destination path or entry name.
892
 * @param const bool force - If true, overwrites an existing destination entry
893
 * when necessary.
894
 * @return void - This function does not return a value.
895
 */
896
void ImitatePass::Move(const QString src, const QString dest,
1✔
897
                       const bool force) {
898
  transactionHelper trans(this, PASS_MOVE);
1✔
899
  QString destFile = resolveMoveDestination(src, dest, force);
1✔
900
  if (destFile.isEmpty()) {
1✔
901
    return;
902
  }
903

904
#ifdef QT_DEBUG
905
  dbg() << "Move Source: " << src;
906
  dbg() << "Move Destination: " << destFile;
907
#endif
908

909
  if (m_settings.useGit) {
1✔
910
    executeMoveGit(src, destFile, force);
×
911
  } else {
912
    QDir qDir;
1✔
913
    if (force) {
1✔
914
      qDir.remove(destFile);
×
915
    }
916
    qDir.rename(src, destFile);
1✔
917
  }
1✔
918
}
919

920
/**
921
 * @brief Copies a file or directory from source to destination, optionally
922
 * forcing overwrite.
923
 * @example
924
 * void result = ImitatePass::Copy(src, dest, force);
925
 *
926
 * @param QString src - Source path to copy from.
927
 * @param QString dest - Destination path to copy to.
928
 * @param bool force - If true, overwrites the destination when it already
929
 * exists.
930
 * @return void - This function does not return a value.
931
 */
932
void ImitatePass::Copy(const QString src, const QString dest,
1✔
933
                       const bool force) {
934
  QFileInfo destFileInfo(dest);
1✔
935
  transactionHelper trans(this, PASS_COPY);
1✔
936
  if (m_settings.useGit) {
1✔
937
    QStringList args;
×
938
    args << "cp";
×
939
    if (force) {
×
940
      args << "-f";
×
941
    }
942
    args << pgit(src);
×
943
    args << pgit(dest);
×
944
    executeGit(GIT_COPY, args);
×
945

946
    QString message = QString("Copied from %1 to %2 using QtPass.");
×
947
    message = message.arg(src, dest);
×
948
    gitCommit("", message);
×
949
  } else {
950
    QDir qDir;
1✔
951
    if (force) {
1✔
952
      qDir.remove(dest);
×
953
    }
954
    QFile::copy(src, dest);
1✔
955
  }
1✔
956
  // reecrypt all files under the new folder
957
  if (destFileInfo.isDir()) {
1✔
958
    reencryptPath(destFileInfo.absoluteFilePath());
×
959
  } else if (destFileInfo.isFile()) {
1✔
960
    reencryptPath(destFileInfo.dir().path());
2✔
961
  }
962
}
1✔
963

964
/**
965
 * @brief ImitatePass::executeGpg easy wrapper for running gpg commands
966
 * @param args
967
 */
968
void ImitatePass::executeGpg(PROCESS id, const QStringList &args, QString input,
29✔
969
                             bool readStdout, bool readStderr) {
970
  executeWrapper(id, m_settings.gpgExecutable, args, std::move(input),
29✔
971
                 readStdout, readStderr);
972
}
29✔
973

974
/**
975
 * @brief ImitatePass::executeGit easy wrapper for running git commands
976
 * @param args
977
 */
978
void ImitatePass::executeGit(PROCESS id, const QStringList &args, QString input,
2✔
979
                             bool readStdout, bool readStderr) {
980
  executeWrapper(id, m_settings.gitExecutable, args, std::move(input),
2✔
981
                 readStdout, readStderr);
982
}
2✔
983

984
/**
985
 * @brief ImitatePass::finished this function is overloaded to ensure
986
 *                              identical behaviour to RealPass ie. only PASS_*
987
 *                              processes are visible inside Pass::finish, so
988
 *                              that interface-wise it all looks the same
989
 * @param id
990
 * @param exitCode
991
 * @param out
992
 * @param err
993
 */
994
void ImitatePass::finished(int id, int exitCode, const QString &out,
31✔
995
                           const QString &err) {
996
#ifdef QT_DEBUG
997
  dbg() << "Imitate Pass";
998
#endif
999
  static QString transactionOutput;
31✔
1000
  PROCESS pid = transactionIsOver(static_cast<PROCESS>(id));
31✔
1001
  transactionOutput.append(out);
31✔
1002

1003
  if (exitCode == 0) {
31✔
1004
    if (pid == INVALID) {
31✔
1005
      return;
1006
    }
1007
  } else {
1008
    while (pid == INVALID) {
×
1009
      id = exec.cancelNext();
×
1010
      if (id == -1) {
×
1011
        //  this is probably irrecoverable and shall not happen
1012
#ifdef QT_DEBUG
1013
        dbg() << "No such transaction!";
1014
#endif
1015
        return;
1016
      }
1017
      pid = transactionIsOver(static_cast<PROCESS>(id));
×
1018
    }
1019
  }
1020
  Pass::finished(pid, exitCode, transactionOutput, err);
29✔
1021
  transactionOutput.clear();
29✔
1022
}
1023

1024
/**
1025
 * @brief Register a transaction before each wrapped execution.
1026
 *
1027
 * Native mode treats every git/gpg invocation as a transaction; the base
1028
 * Pass::executeWrapper calls this hook just before dispatching.
1029
 * @param id Process identifier of the command about to run.
1030
 */
1031
void ImitatePass::beforeExecute(PROCESS id) { transactionAdd(id); }
31✔
1032

1033
/**
1034
 * @brief Decrypt one .gpg file and return lines matching rx.
1035
 */
1036
auto ImitatePass::grepMatchFile(const QProcessEnvironment &env,
9✔
1037
                                const QString &gpgExe, const QString &filePath,
1038
                                const QRegularExpression &rx) -> QStringList {
1039
  QString translatedPath = filePath;
1040
  if (gpgExe.startsWith(QStringLiteral("wsl "))) {
18✔
NEW
1041
    translatedPath = QStringLiteral("$(wslpath ") + filePath + QLatin1Char(')');
×
NEW
1042
    translatedPath.replace('\\', '/');
×
1043
  }
1044
  QString plaintext;
9✔
1045
  const int rc =
1046
      Executor::executeBlocking(env, gpgExe,
81✔
1047
                                {"-d", "--quiet", "--yes", "--no-encrypt-to",
1048
                                 "--batch", "--use-agent", translatedPath},
1049
                                &plaintext);
1050
  if (rc != 0 || plaintext.isEmpty())
9✔
1051
    return {};
3✔
1052
  QStringList matches;
6✔
1053
  for (const QString &line : plaintext.split('\n')) {
30✔
1054
    QString candidate = line;
1055
    if (candidate.endsWith('\r'))
18✔
1056
      candidate.chop(1);
×
1057
    const QString t = candidate.trimmed();
1058
    if (!t.isEmpty() && candidate.contains(rx))
18✔
1059
      matches << t;
1060
  }
1061
  return matches;
1062
}
9✔
1063

1064
/**
1065
 * @brief Walk the store, decrypt every .gpg file, collect matches.
1066
 */
1067
auto ImitatePass::grepScanStore(const QProcessEnvironment &env,
5✔
1068
                                const QString &gpgExe, const QString &storeDir,
1069
                                const QRegularExpression &rx)
1070
    -> QList<QPair<QString, QStringList>> {
1071
  QList<QPair<QString, QStringList>> results;
5✔
1072
  QDirIterator it(storeDir, QStringList() << "*.gpg", QDir::Files,
10✔
1073
                  QDirIterator::Subdirectories);
5✔
1074
  while (it.hasNext()) {
13✔
1075
    if (QThread::currentThread()->isInterruptionRequested())
8✔
1076
      return {};
×
1077
    const QString filePath = it.next();
8✔
1078
    const QStringList matches = grepMatchFile(env, gpgExe, filePath, rx);
8✔
1079
    if (!matches.isEmpty()) {
8✔
1080
      QString entry = QDir(storeDir).relativeFilePath(filePath);
6✔
1081
      if (entry.endsWith(QLatin1String(".gpg")))
6✔
1082
        entry.chop(4);
6✔
1083
      results.append({entry, matches});
6✔
1084
    }
1085
  }
1086
  return results;
1087
}
5✔
1088

1089
/**
1090
 * @brief Search all password content by GPG-decrypting each .gpg file.
1091
 *
1092
 * The pattern is evaluated with `QRegularExpression` (**PCRE**), which differs
1093
 * from the POSIX BRE dialect of the `pass` backend — see Pass::Grep for the
1094
 * cross-backend caveat.
1095
 *
1096
 * Runs a background thread to avoid blocking the UI. Results are emitted on
1097
 * the main thread via QMetaObject::invokeMethod. A sequence counter discards
1098
 * results from superseded searches.
1099
 */
1100
void ImitatePass::Grep(QString pattern, bool caseInsensitive) {
5✔
1101
  for (QThread *t : std::as_const(m_grepThreads))
5✔
1102
    if (t && t->isRunning())
×
1103
      t->requestInterruption();
×
1104
  // No wait() — blocking the UI thread while GPG decrypts would freeze the
1105
  // interface. Stale results are discarded via the sequence counter.
1106

1107
  // Advance the sequence before any early return so in-flight workers from the
1108
  // previous query fail the seq check and cannot publish stale results.
1109
  const int seq = ++m_grepSeq;
5✔
1110

1111
  // Use trimmed() rather than isEmpty(): a whitespace-only string is a valid
1112
  // regex that matches every non-empty line, which is almost never intentional
1113
  // and would decrypt the entire store.
1114
  //
1115
  // Both early returns post finishedGrep via Qt::QueuedConnection so that the
1116
  // signal is always delivered asynchronously after Grep() returns, matching
1117
  // the contract of the threaded path.
1118
  if (pattern.trimmed().isEmpty()) {
5✔
1119
    QMetaObject::invokeMethod(
×
1120
        this,
1121
        [this, seq]() {
×
1122
          if (m_grepSeq == seq)
×
1123
            emit finishedGrep({});
×
1124
        },
×
1125
        Qt::QueuedConnection);
1126
    return;
1✔
1127
  }
1128

1129
  const QRegularExpression rx(
1130
      pattern, caseInsensitive ? QRegularExpression::CaseInsensitiveOption
1131
                               : QRegularExpression::PatternOptions{});
10✔
1132
  if (!rx.isValid()) {
5✔
1133
    QMetaObject::invokeMethod(
1✔
1134
        this,
1135
        [this, seq]() {
1✔
1136
          if (m_grepSeq == seq)
1✔
1137
            emit finishedGrep({});
2✔
1138
        },
1✔
1139
        Qt::QueuedConnection);
1140
    return;
1141
  }
1142
  const QString gpgExe = m_settings.gpgExecutable;
1143
  const QString storeDir = m_settings.passStore;
1144
  const QProcessEnvironment env = exec.environment();
4✔
1145
  QPointer<ImitatePass> self(this);
1146

1147
  auto emitResults = [self, seq](QList<QPair<QString, QStringList>> results) {
8✔
1148
    if (!self)
4✔
1149
      return;
1150
    QMetaObject::invokeMethod(
4✔
1151
        self,
1152
        [self, seq, results = std::move(results)]() {
12✔
1153
          if (self && self->m_grepSeq == seq)
8✔
1154
            emit self->finishedGrep(results);
4✔
1155
        },
4✔
1156
        Qt::QueuedConnection);
1157
  };
4✔
1158

1159
  QThread *thread = QThread::create(
4✔
1160
      [gpgExe, storeDir, env, rx, emitResults = std::move(emitResults)]() {
8✔
1161
        std::move(emitResults)(grepScanStore(env, gpgExe, storeDir, rx));
4✔
1162
      });
4✔
1163

1164
  m_grepThreads.append(thread);
4✔
1165
  connect(thread, &QThread::finished, thread, &QObject::deleteLater);
4✔
1166
  connect(thread, &QThread::finished, this,
4✔
1167
          [this, thread]() { m_grepThreads.removeOne(thread); });
8✔
1168
  thread->start();
4✔
1169
}
9✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc