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

IJHack / QtPass / 24613312963

18 Apr 2026 08:32PM UTC coverage: 22.627% (+0.7%) from 21.908%
24613312963

push

github

web-flow
feat: implement pass grep content search (#109) (#1037)

* feat: implement pass grep content search (#109)

Add full content search (pass grep) for both RealPass and ImitatePass
backends, with a UI toggle button and results panel in the main window.

**RealPass** delegates to `pass grep [-i] <pattern>` and parses the
ANSI-coloured output: entry headers are identified by the `\x1B[94m`
blue-colour escape before stripping ANSI, giving reliable detection
regardless of locale.

**ImitatePass** runs a `QThread::create` background worker that
iterates every `.gpg` file under the store with `QDirIterator`,
decrypts each with `Executor::executeBlocking`, and applies a
`QRegularExpression` to the plaintext. Results are marshalled back
to the main thread via `QMetaObject::invokeMethod(Qt::QueuedConnection)`.
A `QPointer<ImitatePass>` guards against use-after-free if the object
is destroyed while the thread is running.

**UI changes**
- A checkable `grepButton` (QToolButton, `*`) sits next to the search
  field; toggling it switches between filename-filter mode and content-
  search mode and updates the placeholder text.
- `on_lineEdit_textChanged` skips the proxy-model filter in grep mode.
- `on_lineEdit_returnPressed` calls `Pass::Grep()` in grep mode.
- `onGrepFinished` populates a `QTreeWidget` (`grepResultsList`) with
  entry names as top-level items and matching lines as children.
- Clicking a result navigates the treeView to the corresponding entry
  and triggers the normal show-password flow.

Closes #109

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address code review findings for pass grep feature

- enums.h: move PASS_OTP_GENERATE and PASS_GREP before the sentinel
  values PROCESS_COUNT/INVALID so the count remains correct
- pass.cpp: handle PASS_GREP exit code 1 (no matches) as empty results
  instead of routing through processErrorExit; exit codes > 1 are still
  errors
- realpass.cpp: add '--' before the search pattern so patt... (continued)

81 of 243 new or added lines in 7 files covered. (33.33%)

347 existing lines in 7 files now uncovered.

1304 of 5763 relevant lines covered (22.63%)

8.53 hits per line

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

16.07
/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 "qtpasssettings.h"
6
#include "util.h"
7
#include <QDirIterator>
8
#include <QElapsedTimer>
9
#include <QPointer>
10
#include <QRegularExpression>
11
#include <QThread>
12
#include <utility>
13

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

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

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

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

64
static auto pgit(const QString &path) -> QString {
×
65
  if (!QtPassSettings::getGitExecutable().startsWith("wsl ")) {
×
66
    return path;
67
  }
68
  QString res = "$(wslpath " + path + ")";
×
69
  return res.replace('\\', '/');
×
70
}
71

72
static auto pgpg(const QString &path) -> QString {
1✔
73
  if (!QtPassSettings::getGpgExecutable().startsWith("wsl ")) {
3✔
74
    return path;
75
  }
76
  QString res = "$(wslpath " + path + ")";
×
77
  return res.replace('\\', '/');
×
78
}
79

80
/**
81
 * @brief ImitatePass::GitInit git init wrapper
82
 */
83
void ImitatePass::GitInit() {
×
84
  executeGit(GIT_INIT, {"init", pgit(QtPassSettings::getPassStore())});
×
85
}
×
86

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

92
/**
93
 * @brief ImitatePass::GitPull_b git pull wrapper
94
 */
95
void ImitatePass::GitPull_b() {
×
96
  Executor::executeBlocking(QtPassSettings::getGitExecutable(), {"pull"});
×
97
}
×
98

99
/**
100
 * @brief ImitatePass::GitPush git push wrapper
101
 */
102
void ImitatePass::GitPush() {
×
103
  if (QtPassSettings::isUseGit()) {
×
104
    executeGit(GIT_PUSH, {"push"});
×
105
  }
106
}
×
107

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

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

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

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

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

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

244
/**
245
 * @brief Writes the selected users' GPG key IDs to a .gpg-id file.
246
 * @details Opens the specified file for writing, stores the key ID of each
247
 * enabled user on a separate line, and warns if none of the selected users has
248
 * a secret key available.
249
 *
250
 * @param QString &gpgIdFile - Path to the .gpg-id file to be written.
251
 * @param QList<UserInfo> &users - List of users to evaluate and write to the
252
 * file.
253
 * @return void - This function does not return a value.
254
 *
255
 */
256
void ImitatePass::writeGpgIdFile(const QString &gpgIdFile,
×
257
                                 const QList<UserInfo> &users) {
258
  QFile gpgId(gpgIdFile);
×
259
  if (!gpgId.open(QIODevice::WriteOnly | QIODevice::Text)) {
×
260
    emit critical(tr("Cannot update"),
×
261
                  tr("Failed to open .gpg-id for writing."));
×
262
    return;
263
  }
264
  bool secret_selected = false;
265
  for (const UserInfo &user : users) {
×
266
    if (user.enabled) {
×
267
      gpgId.write((user.key_id + "\n").toUtf8());
×
268
      secret_selected |= user.have_secret;
×
269
    }
270
  }
271
  gpgId.close();
×
272
  if (!secret_selected) {
×
273
    emit critical(
×
274
        tr("Check selected users!"),
×
275
        tr("None of the selected keys have a secret key available.\n"
×
276
           "You will not be able to decrypt any newly added passwords!"));
277
  }
278
}
×
279

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

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

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

393
  QString gpgIdFile = path + ".gpg-id";
×
394
  bool addFile = false;
395
  transactionHelper trans(this, PASS_INIT);
×
396
  if (QtPassSettings::isAddGPGId(true)) {
×
397
    QFileInfo checkFile(gpgIdFile);
×
398
    if (!checkFile.exists() || !checkFile.isFile()) {
×
399
      addFile = true;
400
    }
401
  }
×
402
  writeGpgIdFile(gpgIdFile, users);
×
403

404
  if (!signingKeys.isEmpty()) {
×
405
    if (!signGpgIdFile(gpgIdFile, signingKeys)) {
×
406
      return;
407
    }
408
  }
409

410
  if (!QtPassSettings::isUseWebDav() && QtPassSettings::isUseGit() &&
×
411
      !QtPassSettings::getGitExecutable().isEmpty()) {
×
412
    gitAddGpgId(gpgIdFile, gpgIdSigFile, addFile, addSigFile);
×
413
  }
414
  reencryptPath(path);
×
415
}
416

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

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

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

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

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

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

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

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

592
  if (local_lastDecrypt.right(1) != "\n") {
×
593
    local_lastDecrypt += "\n";
×
594
  }
595

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

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

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

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

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

669
  if (!QtPassSettings::isUseWebDav() && QtPassSettings::isUseGit()) {
×
670
    Executor::executeBlocking(QtPassSettings::getGitExecutable(),
×
671
                              {"add", pgit(fileName)});
672
    QString path =
673
        QDir(QtPassSettings::getPassStore()).relativeFilePath(fileName);
×
674
    path.replace(Util::endsWithGpg(), "");
×
675
    Executor::executeBlocking(QtPassSettings::getGitExecutable(),
×
676
                              {"commit", pgit(fileName), "-m",
677
                               "Re-encrypt for " + path + " using QtPass."});
×
678
  }
679

680
  return true;
681
}
×
682

683
/**
684
 * @brief Create git backup commit before re-encryption.
685
 * @return true if backup created or not needed, false if backup failed.
686
 */
687
auto ImitatePass::createBackupCommit() -> bool {
×
688
  if (!QtPassSettings::isUseGit() ||
×
689
      QtPassSettings::getGitExecutable().isEmpty()) {
×
690
    return true;
691
  }
692
  emit statusMsg(tr("Creating backup commit"), 2000);
×
693
  const QString git = QtPassSettings::getGitExecutable();
×
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) {
×
729
  emit statusMsg(tr("Re-encrypting from folder %1").arg(dir), 3000);
×
730
  emit startReencryptPath();
×
731
  if (QtPassSettings::isAutoPull()) {
×
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()) {
×
738
    emit endReencryptPath();
×
739
    return;
×
740
  }
741

742
  QDir currentDir;
×
743
  QDirIterator gpgFiles(dir, QStringList() << "*.gpg", QDir::Files,
×
744
                        QDirIterator::Subdirectories);
×
745
  QStringList gpgIdFilesVerified;
×
746
  QStringList gpgId;
×
747
  int successCount = 0;
748
  int failCount = 0;
749
  while (gpgFiles.hasNext()) {
×
750
    QString fileName = gpgFiles.next();
×
751
    if (gpgFiles.fileInfo().path() != currentDir.path()) {
×
752
      if (!verifyGpgIdForDir(fileName, gpgIdFilesVerified, gpgId)) {
×
753
        emit endReencryptPath();
×
754
        return;
755
      }
756
      if (gpgId.isEmpty() && !gpgIdFilesVerified.isEmpty()) {
×
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);
×
764
    if (actualKeys != gpgId) {
×
765
      if (reencryptSingleFile(fileName, gpgId)) {
×
766
        successCount++;
×
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) {
×
776
    emit statusMsg(tr("Re-encryption completed: %1 succeeded, %2 failed")
×
777
                       .arg(successCount)
×
778
                       .arg(failCount),
×
779
                   5000);
780
  } else {
781
    emit statusMsg(
×
782
        tr("Re-encryption completed: %1 files re-encrypted").arg(successCount),
×
783
        3000);
784
  }
785

786
  if (QtPassSettings::isAutoPush()) {
×
787
    emit statusMsg(tr("Updating password-store"), 2000);
×
788
    GitPush();
×
789
  }
790
  emit endReencryptPath();
×
791
}
×
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,
5✔
808
                                         const QString &dest, bool force)
809
    -> QString {
810
  QFileInfo srcFileInfo(src);
5✔
811
  QFileInfo destFileInfo(dest);
5✔
812
  QString destFile;
5✔
813
  QString srcFileBaseName = srcFileInfo.fileName();
5✔
814

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

830
    if (destFile.endsWith(".gpg", Qt::CaseInsensitive)) {
6✔
831
      destFile.chop(4);
3✔
832
    }
833
    destFile.append(".gpg");
3✔
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 QString();
842
    } else {
843
      destFile = dest;
×
844
    }
845
  } else {
846
#ifdef QT_DEBUG
847
    dbg() << "Source file does not exist";
848
#endif
849
    return QString();
850
  }
851
  return destFile;
852
}
5✔
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

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

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

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

910
  if (QtPassSettings::isUseGit()) {
×
911
    executeMoveGit(src, destFile, force);
×
912
  } else {
913
    QDir qDir;
×
914
    if (force) {
×
915
      qDir.remove(destFile);
×
916
    }
917
    qDir.rename(src, destFile);
×
918
  }
×
919
}
920

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

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

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

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

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

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

1025
/**
1026
 * @brief executeWrapper    overrided so that every execution is a transaction
1027
 * @param id
1028
 * @param app
1029
 * @param args
1030
 * @param input
1031
 * @param readStdout
1032
 * @param readStderr
1033
 */
1034
void ImitatePass::executeWrapper(PROCESS id, const QString &app,
×
1035
                                 const QStringList &args, QString input,
1036
                                 bool readStdout, bool readStderr) {
1037
  transactionAdd(id);
×
1038
  Pass::executeWrapper(id, app, args, input, readStdout, readStderr);
×
1039
}
×
1040

1041
/**
1042
 * @brief Decrypt one .gpg file and return lines matching rx.
1043
 */
1044
auto ImitatePass::grepMatchFile(const QStringList &env, const QString &gpgExe,
1✔
1045
                                const QString &filePath,
1046
                                const QRegularExpression &rx) -> QStringList {
1047
  QString plaintext;
1✔
1048
  const int rc =
1049
      Executor::executeBlocking(env, gpgExe,
9✔
1050
                                {"-d", "--quiet", "--yes", "--no-encrypt-to",
1051
                                 "--batch", "--use-agent", pgpg(filePath)},
1052
                                &plaintext);
1053
  if (rc != 0 || plaintext.isEmpty())
1✔
1054
    return {};
1✔
NEW
1055
  QStringList matches;
×
NEW
1056
  for (const QString &line : plaintext.split('\n')) {
×
1057
    QString candidate = line;
NEW
1058
    if (candidate.endsWith('\r'))
×
NEW
1059
      candidate.chop(1);
×
1060
    const QString t = candidate.trimmed();
NEW
1061
    if (!t.isEmpty() && candidate.contains(rx))
×
1062
      matches << t;
1063
  }
1064
  return matches;
1065
}
1✔
1066

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

1092
/**
1093
 * @brief Search all password content by GPG-decrypting each .gpg file.
1094
 *
1095
 * Runs a background thread to avoid blocking the UI. Results are emitted on
1096
 * the main thread via QMetaObject::invokeMethod. A sequence counter discards
1097
 * results from superseded searches.
1098
 */
1099
void ImitatePass::Grep(QString pattern, bool caseInsensitive) {
2✔
1100
  for (QThread *t : std::as_const(m_grepThreads))
2✔
NEW
1101
    if (t && t->isRunning())
×
NEW
1102
      t->requestInterruption();
×
1103
  // No wait() — blocking the UI thread while GPG decrypts would freeze the
1104
  // interface. Stale results are discarded via the sequence counter.
1105

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

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

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

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

1158
  QThread *thread = QThread::create([gpgExe, storeDir, env, rx, emitResults]() {
1✔
1159
    emitResults(grepScanStore(env, gpgExe, storeDir, rx));
1✔
1160
  });
1✔
1161

1162
  m_grepThreads.append(thread);
1✔
1163
  connect(thread, &QThread::finished, thread, &QObject::deleteLater);
1✔
1164
  connect(thread, &QThread::finished, this,
1✔
1165
          [this, thread]() { m_grepThreads.removeOne(thread); });
2✔
1166
  thread->start();
1✔
1167
}
2✔
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