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

IJHack / QtPass / 35624352350

21 Sep 2026 04:14PM UTC coverage: 79.021% (-0.01%) from 79.031%
35624352350

push

github

web-flow
lupdate + best-effort translations for the generation-conflict strings; GpgIdGeneration contexts lupdate could not see (#1880)

* lupdate + best-effort translations for the generation-conflict strings

lupdate after #1878: six new source texts (the conflict verdict, the
recovery fragment, the record lock and moved-record messages, the
written-but-not-recorded notice). Filled in all 61 locales other than ru,
he and en_US by one agent per locale, reusing each file's terminology;
placeholders checked, lrelease clean. All left type="unfinished" for
Weblate review.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

* GpgIdGeneration: spell the context out where lupdate finds none

The free tr() helper translates under "GpgIdGeneration", but lupdate takes
the context from the enclosing class: in the free functions of the
anonymous namespace it found none and dropped the record messages ("cannot
be accessed", "is not readable", "cannot be written") from every catalog,
and in the Transaction struct it filed the lock message under a context
"Transaction" that nothing looks up. Those call sites use
QCoreApplication::translate("GpgIdGeneration", ...) now; the class's own
methods keep tr(). lupdate re-run, the Transaction context dropped from
the catalogs (lupdate had carried the lock string across by same text),
and the three record messages filled in all 61 locales by the same round.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

2 of 5 new or added lines in 1 file covered. (40.0%)

10 existing lines in 2 files now uncovered.

7375 of 9333 relevant lines covered (79.02%)

80.39 hits per line

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

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

14
NativeGrep::NativeGrep(QObject *parent) : QObject(parent) {}
113 ✔
15

16
NativeGrep::~NativeGrep() {
113 ✔
17
  static constexpr int kThreadTimeoutMs = 5000;
18
  cancel();
113 ✔
19
  QElapsedTimer elapsed;
113 ✔
20
  elapsed.start();
113 ✔
21
  for (const Worker &w : std::as_const(m_workers)) {
113 ✔
UNCOV
22
    if (w.thread && w.thread->isRunning()) {
×
23
      const int remaining =
24
          kThreadTimeoutMs - static_cast<int>(elapsed.elapsed());
×
25
      if (remaining > 0)
×
26
        w.thread->wait(remaining);
×
27
    }
28
  }
29
}
113 ✔
30

31
/**
32
 * @brief Decrypt one .gpg file and return lines matching rx.
33
 */
34
auto NativeGrep::matchFile(const QProcessEnvironment &env,
11 ✔
35
                           const QString &gpgExe, const QString &filePath,
36
                           const QRegularExpression &rx,
37
                           const std::atomic_bool *cancel) -> QStringList {
38
  const QString translatedPath =
39
      Executor::translatePathForWsl(filePath, gpgExe);
11 ✔
40
  QString plaintext;
11 ✔
41
  // The QProcess overload is the one that takes the cancel flag; it polls
42
  // the flag while waiting and terminates (then kills) gpg from this thread,
43
  // the one that owns the process.
44
  QProcess gpg;
11 ✔
45
  gpg.setProcessEnvironment(env);
11 ✔
46
  const int rc =
47
      Executor::executeBlocking(gpg, gpgExe,
99 ✔
48
                                {"-d", "--quiet", "--yes", "--no-encrypt-to",
49
                                 "--batch", "--use-agent", translatedPath},
50
                                QString(), &plaintext, nullptr, cancel);
11 ✔
51
  if (rc != 0 || plaintext.isEmpty())
11 ✔
52
    return {};
4 ✔
53
  QStringList matches;
7 ✔
54
  for (const QString &line : plaintext.split('\n')) {
34 ✔
55
    QString candidate = line;
56
    if (candidate.endsWith('\r'))
20 ✔
57
      candidate.chop(1);
×
58
    const QString t = candidate.trimmed();
59
    if (!t.isEmpty() && candidate.contains(rx))
20 ✔
60
      matches << t;
61
  }
62
  return matches;
63
}
22 ✔
64

65
/**
66
 * @brief Walk the store, decrypt every .gpg file, collect matches.
67
 */
68
auto NativeGrep::scanStore(const QProcessEnvironment &env,
7 ✔
69
                           const QString &gpgExe, const QString &storeDir,
70
                           const QRegularExpression &rx,
71
                           const std::atomic_bool *cancel)
72
    -> QList<QPair<QString, QStringList>> {
73
  QList<QPair<QString, QStringList>> results;
7 ✔
74
  // Regular files only: a link or junction is not an entry, and searching
75
  // through one would decrypt files outside the store.
76
  const QStringList files =
77
      Util::regularFilesUnder(storeDir, QStringList() << "*.gpg");
21 ✔
78
  for (const QString &filePath : files) {
17 ✔
79
    if (QThread::currentThread()->isInterruptionRequested() ||
10 ✔
80
        (cancel != nullptr && cancel->load()))
10 ✔
81
      return {};
×
82
    const QStringList matches = matchFile(env, gpgExe, filePath, rx, cancel);
10 ✔
83
    if (!matches.isEmpty()) {
10 ✔
84
      QString entry = QDir(storeDir).relativeFilePath(filePath);
7 ✔
85
      if (entry.endsWith(QLatin1String(".gpg")))
7 ✔
86
        entry.chop(4);
7 ✔
87
      results.append({entry, matches});
7 ✔
88
    }
89
  }
90
  return results;
91
}
92

93
/**
94
 * @brief Start a search on a worker thread.
95
 *
96
 * Results are emitted on the owner's thread via QMetaObject::invokeMethod. A
97
 * sequence counter discards results from superseded searches; the previous
98
 * search is asked to stop but not waited for, since blocking the UI thread
99
 * while gpg decrypts would freeze the interface.
100
 */
101
void NativeGrep::search(const QString &pattern, bool caseInsensitive,
7 ✔
102
                        const QString &gpgExe, const QString &storeDir,
103
                        const QProcessEnvironment &env) {
104
  cancel();
7 ✔
105

106
  // Advance the sequence before any early return so in-flight workers from the
107
  // previous query fail the seq check and cannot publish stale results.
108
  const int seq = ++m_seq;
7 ✔
109

110
  // Use trimmed() rather than isEmpty(): a whitespace-only string is a valid
111
  // regex that matches every non-empty line, which is almost never intentional
112
  // and would decrypt the entire store.
113
  //
114
  // Both early returns post finished() via Qt::QueuedConnection so that the
115
  // signal is always delivered asynchronously after search() returns, matching
116
  // the contract of the threaded path.
117
  if (pattern.trimmed().isEmpty()) {
7 ✔
118
    QMetaObject::invokeMethod(
×
119
        this,
120
        [this, seq]() {
×
121
          if (m_seq == seq)
×
122
            emit finished({});
×
123
        },
×
124
        Qt::QueuedConnection);
125
    return;
1 ✔
126
  }
127

128
  const QRegularExpression rx(
129
      pattern, caseInsensitive ? QRegularExpression::CaseInsensitiveOption
130
                               : QRegularExpression::PatternOptions{});
14 ✔
131
  if (!rx.isValid()) {
7 ✔
132
    QMetaObject::invokeMethod(
1 ✔
133
        this,
134
        [this, seq]() {
1 ✔
135
          if (m_seq == seq)
1 ✔
136
            emit finished({});
2 ✔
137
        },
1 ✔
138
        Qt::QueuedConnection);
139
    return;
140
  }
141
  QPointer<NativeGrep> self(this);
142

143
  auto emitResults = [self, seq](QList<QPair<QString, QStringList>> results) {
12 ✔
144
    if (!self)
6 ✔
145
      return;
146
    QMetaObject::invokeMethod(
6 ✔
147
        self,
148
        [self, seq, results = std::move(results)]() {
18 ✔
149
          if (self && self->m_seq == seq)
12 ✔
150
            emit self->finished(results);
6 ✔
151
        },
6 ✔
152
        Qt::QueuedConnection);
153
  };
6 ✔
154

155
  auto cancelFlag = std::make_shared<std::atomic_bool>(false);
6 ✔
156
  QThread *thread = QThread::create([gpgExe, storeDir, env, rx, cancelFlag,
12 ✔
157
                                     emitResults = std::move(emitResults)]() {
158
    std::move(emitResults)(
6 ✔
159
        scanStore(env, gpgExe, storeDir, rx, cancelFlag.get()));
6 ✔
160
  });
6 ✔
161

162
  m_workers.append({thread, cancelFlag});
12 ✔
163
  connect(thread, &QThread::finished, thread, &QObject::deleteLater);
6 ✔
164
  connect(thread, &QThread::finished, this, [this, thread]() {
6 ✔
165
    m_workers.removeIf(
6 ✔
166
        [thread](const Worker &w) { return w.thread == thread; });
6 ✔
167
  });
168
  thread->start();
6 ✔
169
}
13 ✔
170

171
void NativeGrep::cancel() {
233 ✔
172
  for (const Worker &w : std::as_const(m_workers)) {
234 ✔
173
    w.cancel->store(true);
174
    if (w.thread && w.thread->isRunning())
1 ✔
175
      w.thread->requestInterruption();
1 ✔
176
  }
177
}
233 ✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc