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

IJHack / QtPass / 40581989579

27 Oct 2026 08:53PM UTC coverage: 92.492% (+0.03%) from 92.461%
40581989579

Pull #2070

github

web-flow
Merge 03a8fd3b6 into f5d21f83d
Pull Request #2070: Sign git commits when the store asks, as pass does (pass.signcommits)

68 of 69 new or added lines in 3 files covered. (98.55%)

1 existing line in 1 file now uncovered.

9486 of 10256 relevant lines covered (92.49%)

303.51 hits per line

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

98.61
/src/profileinit.cpp
1
// SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "profileinit.h"
4
#include "appsettings.h"
5
#include "executor.h"
6
#include "gpgidgeneration.h"
7
#include "gpgidsigner.h"
8
#include "qtpasslogging.h"
9
#include "userinfo.h"
10
#include "util.h"
11
#include <QDir>
12
#include <QFile>
13
#include <QFileInfo>
14
#include <QProcess>
15

16
namespace {
17
/// As Pass::updateEnv() sets it for the active store: git in @p dir does not
18
/// look above it, so neither the identity check nor a commit reads or uses a
19
/// repository the new store merely lies in (a dotfiles one in the home
20
/// folder), unless the user opted into that; and paths are names to it, not
21
/// patterns.
22
auto gitEnvironment(const QString &dir, const AppSettings &s)
25 ✔
23
    -> QProcessEnvironment {
24
  QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
25 ✔
25
  // As for the active store: the paths handed to git are names, not
26
  // patterns (see Pass::updateEnv()).
27
  env.insert(QStringLiteral("GIT_LITERAL_PATHSPECS"), QStringLiteral("1"));
50 ✔
28
  if (!s.gitUseParentRepository) {
25 ✔
29
    env.insert(QStringLiteral("GIT_CEILING_DIRECTORIES"),
48 ✔
30
               Util::gitCeiling(dir));
48 ✔
31
  }
32
  return env;
25 ✔
33
}
×
34
/// "commit", and "-S" when git config pass.signcommits is true for the
35
/// repository @p git runs in (a new one: so the user's own git config), as
36
/// pass signs its commits.
37
auto commitArgs(QProcess &git, const AppSettings &s) -> QStringList {
6 ✔
38
  QString out;
6 ✔
39
  const bool sign =
40
      Executor::executeBlocking(
36 ✔
41
          git, s.gitExecutable,
6 ✔
42
          {QStringLiteral("config"), QStringLiteral("--bool"),
6 ✔
43
           QStringLiteral("--get"), QStringLiteral("pass.signcommits")},
6 ✔
44
          QString(), &out, nullptr) == 0 &&
6 ✔
45
      out.trimmed() == QLatin1String("true");
6 ✔
46
  return sign ? QStringList{QStringLiteral("commit"), QStringLiteral("-S")}
12 ✔
47
              : QStringList{QStringLiteral("commit")};
18 ✔
48
}
49
} // namespace
50

51
auto ProfileInit::needsInit(const QString &path) -> bool {
11 ✔
52
  if (path.isEmpty()) {
11 ✔
53
    return false;
54
  }
55
  QDir dir(path);
10 ✔
56
  if (!dir.exists()) {
10 ✔
57
    return false;
58
  }
59
  return !dir.exists(".gpg-id");
9 ✔
60
}
10 ✔
61

62
auto ProfileInit::initialise(const QString &dir, const QList<UserInfo> &users,
18 ✔
63
                             const AppSettings &s, bool useGit, QString *note)
64
    -> bool {
65
  QString scratch;
18 ✔
66
  QString &out = note != nullptr ? *note : scratch;
18 ✔
67
  out.clear();
18 ✔
68

69
  const QDir folder(dir);
18 ✔
70
  if (!folder.exists() && !Util::mkpathForStore(folder.absolutePath())) {
22 ✔
71
    out = tr("Could not create %1.").arg(folder.absolutePath());
2 ✔
72
    return false;
1 ✔
73
  }
74
  const QString gpgIdFile = folder.filePath(QStringLiteral(".gpg-id"));
34 ✔
75
  QByteArray written;
17 ✔
76
  if (!writeGpgId(gpgIdFile, users, !s.passSigningKey.trimmed().isEmpty(), &out,
34 ✔
77
                  &written)) {
78
    return false;
79
  }
80
  QString sigFile;
14 ✔
81
  if (!s.passSigningKey.trimmed().isEmpty()) {
14 ✔
82
    if (!signGpgId(gpgIdFile, written, s, &out)) {
4 ✔
83
      return false;
84
    }
85
    sigFile = gpgIdFile + QStringLiteral(".sig");
2 ✔
86
  }
87
  if (useGit &&
15 ✔
88
      !commitGpgId(folder.absolutePath(), gpgIdFile, sigFile, s, &out)) {
15 ✔
89
    return false;
90
  }
91

92
  const QStringList existing = folder.entryList(
9 ✔
93
      {QStringLiteral("*.gpg")}, QDir::Files | QDir::NoDotAndDotDot);
27 ✔
94
  if (!existing.isEmpty()) {
9 ✔
95
    out = tr("%1 already contains %n encrypted file(s); they were not "
3 ✔
96
             "re-encrypted. Switch to the profile and open Users to do that.",
97
             nullptr, static_cast<int>(existing.size()))
98
              .arg(folder.absolutePath());
6 ✔
99
  }
100
  return true;
101
}
18 ✔
102

103
auto ProfileInit::writeGpgId(const QString &gpgIdFile,
17 ✔
104
                             const QList<UserInfo> &users, bool signed_,
105
                             QString *note, QByteArray *written) -> bool {
106
  QStringList ids;
17 ✔
107
  for (const UserInfo &user : users) {
47 ✔
108
    if (user.enabled) {
30 ✔
109
      ids << user.key_id;
16 ✔
110
    }
111
  }
112
  if (ids.isEmpty()) {
17 ✔
113
    *note = tr("No recipient selected; %1 was not written.").arg(gpgIdFile);
2 ✔
114
    return false;
1 ✔
115
  }
116
  // As ImitatePass::writeGpgIdFile: renamed into place whole, so an
117
  // interrupted write leaves no half .gpg-id to sign or use. Owner-only: the
118
  // list leaks which keys the store is encrypted to.
119
  QByteArray contents =
120
      (ids.join(QLatin1Char('\n')) + QLatin1Char('\n')).toUtf8();
16 ✔
121
  if (signed_) {
16 ✔
122
    // Same rule as ImitatePass::writeGpgIdFile: reserved and recorded
123
    // before the write, bound to the store root (GpgIdGeneration). A new
124
    // profile has no list on disk to take a generation from.
125
    QString why;
5 ✔
126
    const std::optional<qint64> generation =
127
        GpgIdGeneration::reserveNext(gpgIdFile, std::nullopt, &why);
5 ✔
128
    if (!generation) {
5 ✔
129
      *note = why;
1 ✔
130
      return false;
131
    }
132
    contents =
133
        GpgIdGeneration::withHeader(*generation, QStringLiteral("."), contents);
8 ✔
134
  }
135
  QString why;
15 ✔
136
  if (!Util::writeFileReplacing(gpgIdFile, contents, true, &why)) {
15 ✔
137
    *note = why;
1 ✔
138
    return false;
1 ✔
139
  }
140
  if (written != nullptr) {
14 ✔
141
    *written = contents;
14 ✔
142
  }
143
  if (signed_) {
14 ✔
144
    // Same as ImitatePass::writeGpgIdFile: the bytes written are the ones
145
    // recognised; not fatal, the first read digests them otherwise.
146
    QString why;
4 ✔
147
    if (!GpgIdGeneration::recordWritten(gpgIdFile, contents, &why)) {
4 ✔
UNCOV
148
      qCWarning(lcQtPass) << "Could not record the written .gpg-id:" << why;
×
149
    }
150
  }
151
  return true;
152
}
153

154
auto ProfileInit::signGpgId(const QString &gpgIdFile,
4 ✔
155
                            const QByteArray &contents, const AppSettings &s,
156
                            QString *note) -> bool {
157
  const GpgIdSigner signer(s.gpgExecutable,
158
                           GpgIdSigner::keysFromSetting(s.passSigningKey));
12 ✔
159
  QString err;
4 ✔
160
  if (!signer.sign(gpgIdFile, contents, &err)) {
4 ✔
161
    *note = err.trimmed().isEmpty()
3 ✔
162
                ? tr("Could not sign %1 with %2.")
9 ✔
163
                      .arg(gpgIdFile, signer.keys().first())
164
                : tr("Could not sign %1 with %2: %3")
3 ✔
165
                      .arg(gpgIdFile, signer.keys().first(), err.trimmed());
5 ✔
166
    return false;
3 ✔
167
  }
168
  return true;
169
}
4 ✔
170

171
auto ProfileInit::commitGpgId(const QString &dir, const QString &gpgIdFile,
4 ✔
172
                              const QString &sigFile, const AppSettings &s,
173
                              QString *note) -> bool {
174
  // A process of our own in the profile directory: nothing here may touch
175
  // the shared Executor or PASSWORD_STORE_DIR of the active store.
176
  QProcess git;
4 ✔
177
  git.setWorkingDirectory(dir);
4 ✔
178
  git.setProcessEnvironment(gitEnvironment(dir, s));
4 ✔
179
  auto run = [&](const QStringList &args) -> bool {
8 ✔
180
    QString err;
8 ✔
181
    const int rc = Executor::executeBlocking(git, s.gitExecutable, args,
8 ✔
182
                                             QString(), nullptr, &err);
8 ✔
183
    if (rc != 0) {
8 ✔
184
      *note =
185
          tr("git %1 failed in %2: %3").arg(args.first(), dir, err.trimmed());
4 ✔
186
      return false;
2 ✔
187
    }
188
    return true;
189
  };
4 ✔
190
  QStringList files{QFileInfo(gpgIdFile).fileName()};
12 ✔
191
  if (!sigFile.isEmpty()) {
4 ✔
192
    files << QFileInfo(sigFile).fileName();
2 ✔
193
  }
194
  const QStringList add =
195
      QStringList{QStringLiteral("add"), QStringLiteral("--")} + files;
16 ✔
196
  if (!run({QStringLiteral("init")}) || !run(add)) {
12 ✔
197
    return false;
198
  }
199
  return run(commitArgs(git, s) +
6 ✔
200
             QStringList{QStringLiteral("-m"),
10 ✔
201
                         QStringLiteral("Added .gpg-id using QtPass."),
2 ✔
202
                         QStringLiteral("--")} +
10 ✔
203
             files);
204
}
8 ✔
205

206
auto ProfileInit::initGit(const QString &dir, const AppSettings &s,
6 ✔
207
                          QString *note) -> bool {
208
  QProcess git;
6 ✔
209
  git.setWorkingDirectory(dir);
6 ✔
210
  git.setProcessEnvironment(gitEnvironment(dir, s));
6 ✔
211
  auto run = [&](const QStringList &args) -> bool {
14 ✔
212
    QString err;
14 ✔
213
    const int rc = Executor::executeBlocking(git, s.gitExecutable, args,
14 ✔
214
                                             QString(), nullptr, &err);
14 ✔
215
    if (rc != 0) {
14 ✔
216
      *note =
217
          tr("git %1 failed in %2: %3").arg(args.first(), dir, err.trimmed());
6 ✔
218
      return false;
3 ✔
219
    }
220
    return true;
221
  };
6 ✔
222
  // Stage only store files (as the re-encryption backup commit does): an
223
  // existing folder may hold plaintext exports or swap files. Regular files
224
  // only: a link or junction must not have its target staged.
225
  QStringList files;
6 ✔
226
  const QDir base(dir);
6 ✔
227
  // Hidden directories (.git among them) are not walked, hidden files are:
228
  // .gpg-id is one.
229
  const QStringList found = Util::regularFilesUnder(
18 ✔
230
      dir,
231
      {QStringLiteral("*.gpg"), QStringLiteral(".gpg-id"),
6 ✔
232
       QStringLiteral(".gpg-id.sig")},
6 ✔
233
      nullptr, true);
36 ✔
234
  for (const QString &path : found) {
13 ✔
235
    files << base.relativeFilePath(path);
14 ✔
236
  }
237
  if (!run({QStringLiteral("init")})) {
18 ✔
238
    return false;
239
  }
240
  if (!files.isEmpty() &&
9 ✔
241
      !run(QStringList{QStringLiteral("add"), QStringLiteral("--")} + files)) {
29 ✔
242
    return false;
243
  }
244
  return run(commitArgs(git, s) +
12 ✔
245
             QStringList{QStringLiteral("--allow-empty"), QStringLiteral("-m"),
24 ✔
246
                         QStringLiteral("Added password store using QtPass.")});
16 ✔
247
}
12 ✔
248

249
auto ProfileInit::gitIdentityConfigured(const QString &dir,
15 ✔
250
                                        const AppSettings &s) -> bool {
251
  QProcess git;
15 ✔
252
  git.setWorkingDirectory(dir);
15 ✔
253
  git.setProcessEnvironment(gitEnvironment(dir, s));
15 ✔
254
  for (const char *key : {"user.name", "user.email"}) {
34 ✔
255
    QString out;
25 ✔
256
    if (Executor::executeBlocking(git, s.gitExecutable,
125 ✔
257
                                  {QStringLiteral("config"),
25 ✔
258
                                   QStringLiteral("--get"),
25 ✔
259
                                   QString::fromLatin1(key)},
260
                                  QString(), &out, nullptr) != 0 ||
69 ✔
261
        out.trimmed().isEmpty()) {
25 ✔
262
      return false;
263
    }
264
  }
265
  return true;
9 ✔
266
}
65 ✔
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