Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package QtPass for openSUSE:Factory checked in at 2026-10-07 19:32:04 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/QtPass (Old) and /work/SRC/openSUSE:Factory/.QtPass.new.3698736 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "QtPass" Wed Oct 7 19:32:04 2026 rev:11 rq:1382843 version:1.8.3 Changes: -------- --- /work/SRC/openSUSE:Factory/QtPass/QtPass.changes 2026-09-28 10:41:00.655274405 +0200 +++ /work/SRC/openSUSE:Factory/.QtPass.new.3698736/QtPass.changes 2026-10-07 19:32:42.100882218 +0200 @@ -1,0 +2,12 @@ +Tue Oct 06 15:58:08 UTC 2026 - ecsos <[email protected]> + +- Update to version 1.8.3: + * Release 1.8.3: dated 2026-10-06 (#1971) + * Release 1.8.3 (#1969) + * 1.8: a save that never reports must not trap the dialog (ported from #1965) (#1966) + * 1.8 ci: drop the Qt 6.11.2 pin, 6.11.3's checksums are served (#1964) + * metainfo: real sizes for the qtpass.org screenshots (#1954) + * 1.8: keep the entry when saving it fails (ported from #1951) (#1952) + * 1.8 Flatpak: build v1.8.2, private store, as on Flathub (#1935) + +------------------------------------------------------------------- Old: ---- QtPass-1.8.2.tar.xz New: ---- QtPass-1.8.3.tar.xz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ QtPass.spec ++++++ --- /var/tmp/diff_new_pack.Mv9VMS/_old 2026-10-07 19:32:42.840913123 +0200 +++ /var/tmp/diff_new_pack.Mv9VMS/_new 2026-10-07 19:32:42.841913165 +0200 @@ -18,7 +18,7 @@ %define _name qtpass Name: QtPass -Version: 1.8.2 +Version: 1.8.3 Release: 0 Summary: A multi-platform gui for pass License: GPL-3.0-only ++++++ QtPass-1.8.2.tar.xz -> QtPass-1.8.3.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/QtPass-1.8.2/CHANGELOG.md new/QtPass-1.8.3/CHANGELOG.md --- old/QtPass-1.8.2/CHANGELOG.md 2026-09-24 23:00:10.000000000 +0200 +++ new/QtPass-1.8.3/CHANGELOG.md 2026-10-06 13:09:31.000000000 +0200 @@ -1,5 +1,19 @@ # Changelog +## [1.8.3](https://github.com/IJHack/QtPass/tree/v1.8.3) (2026-10-06) + +### Bugfixes + +- Saving an entry no longer loses what was typed when it fails. The password + dialog closed as soon as OK was pressed and gpg ran afterwards, so a missing + or expired recipient key or an unusable `.gpg-id` lost the entry. The dialog + now stays open until the entry is written, locked and showing "Saving…"; on + failure the fields come back with the reason. Cancel, Esc and closing the window + still work while it waits, after asking, so a save that never reports (an + unanswered PIN prompt) cannot trap you + ([#1944](https://github.com/IJHack/QtPass/issues/1944), ported from + [#1951](https://github.com/IJHack/QtPass/pull/1951)) + ## [1.8.2](https://github.com/IJHack/QtPass/tree/v1.8.2) (2026-09-24) ### Changed @@ -10,7 +24,7 @@ file themselves need the new name; the Flatpak manifest drops `rename-appdata-file` with this tag -### Bugfixes +### Bugfixes <!-- markdownlint-disable-line MD024 --> - "Automatically push" and "Automatically pull" in the settings did nothing since 1.8.0: [#1140](https://github.com/IJHack/QtPass/pull/1140) started diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/QtPass-1.8.2/Doxyfile new/QtPass-1.8.3/Doxyfile --- old/QtPass-1.8.2/Doxyfile 2026-09-24 23:00:10.000000000 +0200 +++ new/QtPass-1.8.3/Doxyfile 2026-10-06 13:09:31.000000000 +0200 @@ -48,7 +48,7 @@ # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 1.8.2 +PROJECT_NUMBER = 1.8.3 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewers a diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/QtPass-1.8.2/flatpak/README.md new/QtPass-1.8.3/flatpak/README.md --- old/QtPass-1.8.2/flatpak/README.md 2026-09-24 23:00:10.000000000 +0200 +++ new/QtPass-1.8.3/flatpak/README.md 2026-10-06 13:09:31.000000000 +0200 @@ -29,8 +29,9 @@ ## How GnuPG works inside the sandbox The sandbox has no access to `~/.gnupg`. The runtime-provided `gpg` (only the -`gpg2` wrapper is ours) therefore keeps its own keyring in `~/.var/app/org.qtpass.QtPass/.gnupg`, but it connects to the -**host** `gpg-agent` through the read-only exposed socket directory +`gpg2` wrapper is ours) therefore keeps its own keyring in +`~/.var/app/org.qtpass.QtPass/.gnupg`, but it connects to the **host** +`gpg-agent` through the read-only exposed socket directory (`--filesystem=xdg-run/gnupg:ro`). Private keys, pinentry dialogs and smartcards stay on the host; only the public keys have to be known inside the sandbox. Import them once: @@ -43,31 +44,47 @@ or paste them via **Users → Import key → From clipboard** in QtPass. The host agent must be running (it is, on any desktop where `gpg` already works). +## The password store + +The default store is private to the Flatpak: +`~/.var/app/org.qtpass.QtPass/.password-store` (`--persist=.password-store`). +To use an existing store, for example `~/.password-store` shared with `pass` +on the host, pick it with the folder button in Settings (Profiles page); the +portal gives access to that folder only. Alternatively, grant the folder to +QtPass alone with Flatseal or: + +```sh +flatpak override --user --filesystem=~/.password-store org.qtpass.QtPass +``` + ## Permissions, and what is deliberately left out -Granted: Wayland/X11, network (for `git` over https), `~/.password-store`, -the gpg-agent socket directory (read-only), a persistent private `~/.gnupg`, -and the tray (`org.kde.StatusNotifierWatcher`). Nothing here needs a -Flathub linter exception. - -Not granted on purpose: `~/.gnupg` itself, `~/.ssh` and the ssh-agent socket. -A store in another location can be chosen through the file dialog (the portal -grants access to the chosen folder); for `git` over SSH add -`--socket=ssh-auth` (and `--filesystem=~/.ssh:ro` for `known_hosts`) with -Flatseal or `flatpak override --user`. +Granted: Wayland/X11, network (for `git` over https), the gpg-agent socket +directory (read-only), a persistent private `~/.password-store` and +`~/.gnupg`, and the tray (`org.kde.StatusNotifierWatcher`). Nothing here needs +a Flathub linter exception. + +Not granted on purpose: any host directory, `~/.gnupg` itself, `~/.ssh` and +the ssh-agent socket. For `git` over SSH, grant them with Flatseal or: + +```sh +flatpak override --user --socket=ssh-auth --filesystem=~/.ssh:ro org.qtpass.QtPass +``` + +(`~/.ssh` read-only is for `known_hosts`.) Without the application ID, +`flatpak override` changes the defaults of every installed Flatpak. ## Updating Bump the `tag`/`commit` of the `qtpass` module to the new release. The `x-checker-data` blocks let Flathub's external-data-checker propose updates of -the bundled tools automatically. The `file` overlays in the `qtpass` module -(metainfo, square icon, `main.cpp`, `qtpasssettings.cpp`, and for the 1.8.1 -first-run fix `mainwindow.h`, `mainwindow.cpp`, `qtpass.cpp`) exist only because -v1.8.0 predates those changes — drop them when building from a later tag. -Since 1.8.2 the metainfo is installed under its component ID -(`org.qtpass.QtPass.metainfo.xml`): drop `rename-appdata-file` when the -module moves to the 1.8.2 tag (`ci-manifest.sh` strips it for the tree build -meanwhile). +the bundled tools automatically. The module builds the tag as-is: since +v1.8.1 the tree ships `flatpak/gpg2`, and `make -C main install` puts the +desktop file, metainfo and icons where `rename-desktop-file` / +`rename-appdata-file` / `rename-icon` expect them, so no working-tree files +are overlaid any more. Since 1.8.2 the metainfo is installed under its +component ID (`org.qtpass.QtPass.metainfo.xml`), so the manifest no longer +needs `rename-appdata-file`. ## CI diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/QtPass-1.8.2/flatpak/org.qtpass.QtPass.yml new/QtPass-1.8.3/flatpak/org.qtpass.QtPass.yml --- old/QtPass-1.8.2/flatpak/org.qtpass.QtPass.yml 2026-09-24 23:00:10.000000000 +0200 +++ new/QtPass-1.8.3/flatpak/org.qtpass.QtPass.yml 2026-10-06 13:09:31.000000000 +0200 @@ -1,20 +1,5 @@ # SPDX-FileCopyrightText: 2026 Anne Jan Brouwer # SPDX-License-Identifier: GPL-3.0-or-later -# -# Flatpak manifest for QtPass. Build and install locally with: -# -# flatpak run org.flatpak.Builder --user --install --force-clean \ -# --install-deps-from=flathub build-dir flatpak/org.qtpass.QtPass.yml -# -# GnuPG comes from the KDE runtime. The sandboxed gpg keeps its own keyring -# (persisted under ~/.var/app/org.qtpass.QtPass/.gnupg) but talks to the HOST -# gpg-agent through the read-only exposed socket directory, so private keys, -# pinentry and smartcards stay on the host. Import your public key once: -# -# gpg --export --armor [email protected] | \ -# flatpak run --command=gpg2 org.qtpass.QtPass --import -# -# or use Users -> Import key -> From clipboard inside QtPass. id: org.qtpass.QtPass runtime: org.kde.Platform runtime-version: "6.11" @@ -22,23 +7,15 @@ command: qtpass rename-desktop-file: qtpass.desktop rename-icon: qtpass-icon -# v1.8.1 installs the metainfo as qtpass.appdata.xml; from 1.8.2 on it is -# installed under the component id. Drop this line with the 1.8.2 tag. -rename-appdata-file: qtpass.appdata.xml finish-args: - --share=ipc - --socket=fallback-x11 - --socket=wayland - --device=dri - # git pull/push to remotes (https; add ssh-auth via Flatseal if you need ssh) - --share=network - # the pass default store location; pick another one through the file dialog - - --filesystem=~/.password-store:create - # host gpg-agent socket (connect works through a read-only bind mount) - --filesystem=xdg-run/gnupg:ro - # persistent keyring for the sandboxed gpg, at its default homedir + - --persist=.password-store - --persist=.gnupg - # tray icon - --talk-name=org.kde.StatusNotifierWatcher cleanup: - /include @@ -104,7 +81,6 @@ - name: qrencode buildsystem: cmake-ninja config-opts: - # 4.1.1 still says cmake_minimum_required(2.8); CMake 4 refuses that - -DCMAKE_POLICY_VERSION_MINIMUM=3.5 - -DWITH_TOOLS=YES - -DWITH_TESTS=NO @@ -124,7 +100,6 @@ build-commands: - make PREFIX=/app WITH_ALLCOMP=no install sources: - # cgit snapshot tarballs are not checksum-stable; pin the tag's commit - type: git url: https://git.zx2c4.com/password-store tag: "1.7.4" @@ -133,22 +108,15 @@ - name: qtpass buildsystem: simple build-commands: - # the KDE SDK ships Qt 6 qmake as plain "qmake" - qmake PREFIX=/app CONFIG+=release qtpass.pro - make -j$FLATPAK_BUILDER_N_JOBS sub-src sub-main - # installs bin/qtpass, the desktop file, the metainfo and the hicolor - # icons under the upstream names; rename-desktop-file / rename-icon - # (and rename-appdata-file, for v1.8.1) rewrite them to the app id - make -C main install - # QtPassSettings::initExecutables() (and pass) look for "gpg2"; the runtime - # ships "gpg". The wrapper also scrubs sandbox-only env before the host - # gpg-agent sees it — see the comments in flatpak/gpg2. - install -Dm755 flatpak/gpg2 /app/bin/gpg2 sources: - type: git url: https://github.com/IJHack/QtPass.git - tag: v1.8.1 - commit: 1a389718013586bf21e3b6d603d437f57a055c88 + tag: v1.8.2 + commit: 71122182ddaef33d83c60868a7baff4a85442f1a x-checker-data: type: git tag-pattern: ^v([\d.]+)$ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/QtPass-1.8.2/org.qtpass.QtPass.metainfo.xml new/QtPass-1.8.3/org.qtpass.QtPass.metainfo.xml --- old/QtPass-1.8.2/org.qtpass.QtPass.metainfo.xml 2026-09-24 23:00:10.000000000 +0200 +++ new/QtPass-1.8.3/org.qtpass.QtPass.metainfo.xml 2026-10-06 13:09:31.000000000 +0200 @@ -62,15 +62,21 @@ <screenshot type="default"> <caption>QtPass main window</caption> <caption xml:lang="nl_NL">QtPass hoofdscherm</caption> - <image width="768" height="596">https://qtpass.org/images/qtpass.png</image> + <image width="1111" height="961">https://qtpass.org/images/qtpass.png</image> </screenshot> <screenshot> <caption>QtPass Configuration window</caption> <caption xml:lang="nl_NL">QtPass Configuratie scherm</caption> - <image width="900" height="637">https://qtpass.org/images/config.png</image> + <image width="822" height="921">https://qtpass.org/images/config.png</image> </screenshot> </screenshots> <releases> + <release version="1.8.3" date="2026-10-06"> + <url>https://github.com/IJHack/QtPass/releases/tag/v1.8.3</url> + <description> + <p>Bug-fix release: a failed save no longer loses what was typed. The password dialog stays open until the entry is written and shows why when encryption fails, and a save that never finishes can still be cancelled.</p> + </description> + </release> <release version="1.8.2" date="2026-09-24"> <url>https://github.com/IJHack/QtPass/releases/tag/v1.8.2</url> <description> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/QtPass-1.8.2/qtpass.iss new/QtPass-1.8.3/qtpass.iss --- old/QtPass-1.8.2/qtpass.iss 2026-09-24 23:00:10.000000000 +0200 +++ new/QtPass-1.8.3/qtpass.iss 2026-10-06 13:09:31.000000000 +0200 @@ -1,6 +1,6 @@ #define MyAppName "QtPass" #ifndef MyAppVersion - #define MyAppVersion "1.8.2" + #define MyAppVersion "1.8.3" #endif #define MyAppPublisher "IJHack" #define MyAppURL "https://qtpass.org/" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/QtPass-1.8.2/qtpass.pri new/QtPass-1.8.3/qtpass.pri --- old/QtPass-1.8.2/qtpass.pri 2026-09-24 23:00:10.000000000 +0200 +++ new/QtPass-1.8.3/qtpass.pri 2026-10-06 13:09:31.000000000 +0200 @@ -1,7 +1,7 @@ # QtPass - GUI for pass # SPDX-FileCopyrightText: 2014 Anne Jan Brouwer -VERSION = 1.8.2 +VERSION = 1.8.3 CONFIG(coverage) { QMAKE_LFLAGS += --coverage diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/QtPass-1.8.2/qtpass.spec new/QtPass-1.8.3/qtpass.spec --- old/QtPass-1.8.2/qtpass.spec 2026-09-24 23:00:10.000000000 +0200 +++ new/QtPass-1.8.3/qtpass.spec 2026-10-06 13:09:31.000000000 +0200 @@ -7,7 +7,7 @@ # This should probably be part of a release process. Name: qtpass -Version: 1.8.2 +Version: 1.8.3 Release: 1%{?dist} Summary: QtPass is a multi-platform GUI for pass, the standard unix password manager. License: GPLv3 @@ -61,6 +61,9 @@ /usr/bin/gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || : %changelog +* Tue Oct 06 2026 Anne Jan Brouwer <[email protected]> 1.8.3 +- Updated QtPass + * Thu Sep 24 2026 Anne Jan Brouwer <[email protected]> 1.8.2 - Updated QtPass diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/QtPass-1.8.2/src/imitatepass.cpp new/QtPass-1.8.3/src/imitatepass.cpp --- old/QtPass-1.8.2/src/imitatepass.cpp 2026-09-24 23:00:10.000000000 +0200 +++ new/QtPass-1.8.3/src/imitatepass.cpp 2026-10-06 13:09:31.000000000 +0200 @@ -146,6 +146,7 @@ * @param overwrite whether to overwrite existing file */ void ImitatePass::Insert(QString file, QString newValue, bool overwrite) { + m_insertWritten = false; file = file + ".gpg"; QString gpgIdPath = Pass::getGpgIdPath(file, m_settings.passStore); if (!verifyGpgIdFile(gpgIdPath)) { @@ -1158,6 +1159,10 @@ #ifdef QT_DEBUG dbg() << "Imitate Pass"; #endif + if (id == PASS_INSERT && exitCode == 0) { + // gpg writes the entry itself (--output); git add and commit follow. + m_insertWritten = true; + } PROCESS pid = transactionIsOver(static_cast<PROCESS>(id)); m_transactionOutput.append(out); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/QtPass-1.8.2/src/imitatepass.h new/QtPass-1.8.3/src/imitatepass.h --- old/QtPass-1.8.2/src/imitatepass.h 2026-09-24 23:00:10.000000000 +0200 +++ new/QtPass-1.8.3/src/imitatepass.h 2026-10-06 13:09:31.000000000 +0200 @@ -201,6 +201,17 @@ const QString &err) override; /** + * @brief Whether the failed insert's gpg step had written the entry + * before a later git step failed. + * @return true when the entry is in the store. + */ + [[nodiscard]] auto insertWritten() const -> bool override { + return m_insertWritten; + } + /// The last Insert()'s gpg wrote its entry; its git steps may still fail. + bool m_insertWritten = false; + + /** * @brief Open a transaction for every execution. * * In native (imitate) mode each wrapped command is a git/gpg transaction; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/QtPass-1.8.2/src/pass.cpp new/QtPass-1.8.3/src/pass.cpp --- old/QtPass-1.8.2/src/pass.cpp 2026-09-24 23:00:10.000000000 +0200 +++ new/QtPass-1.8.3/src/pass.cpp 2026-10-06 13:09:31.000000000 +0200 @@ -726,10 +726,11 @@ if (pid == PASS_INSERT) { const QString friendly = gpgErrorMessage(err); - if (!friendly.isEmpty()) { - emit processErrorExit(exitCode, formatInsertError(friendly, err)); - return; - } + const QString why = + friendly.isEmpty() ? err : formatInsertError(friendly, err); + emit insertFailed(why, insertWritten()); + emit processErrorExit(exitCode, why); + return; } emit processErrorExit(exitCode, err); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/QtPass-1.8.2/src/pass.h new/QtPass-1.8.3/src/pass.h --- old/QtPass-1.8.2/src/pass.h 2026-09-24 23:00:10.000000000 +0200 +++ new/QtPass-1.8.3/src/pass.h 2026-10-06 13:09:31.000000000 +0200 @@ -275,6 +275,14 @@ protected: /** + * @brief Whether the insert that just failed had put its entry in the + * store before a later step failed. The backend that writes the entry + * knows; this default says no. + * @return true when the failed insert's entry was written. + */ + [[nodiscard]] virtual auto insertWritten() const -> bool { return false; } + + /** * @brief Execute external wrapper command. * @param id Process identifier. * @param app Executable path. @@ -382,6 +390,17 @@ void processErrorExit(int exitCode, const QString &err); /** + * @brief Emitted when an Insert() fails after it started a process, + * alongside the generic processErrorExit(); lets the password + * dialog keep what was typed and say why, without reacting to any + * other command failing meanwhile. + * @param err What gpg or pass reported. + * @param written true when this insert did put the entry in the store and + * only a later step (git add or commit) failed. + */ + void insertFailed(const QString &err, bool written); + + /** * @brief Emitted when any operation finishes. * @param out Standard output from the process. * @param err Standard error from the process. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/QtPass-1.8.2/src/passworddialog.cpp new/QtPass-1.8.3/src/passworddialog.cpp --- old/QtPass-1.8.2/src/passworddialog.cpp 2026-09-24 23:00:10.000000000 +0200 +++ new/QtPass-1.8.3/src/passworddialog.cpp 2026-10-06 13:09:31.000000000 +0200 @@ -17,6 +17,7 @@ #include <QIcon> #include <QLabel> #include <QLineEdit> +#include <QMessageBox> #include <QPushButton> #include <QShortcut> #include <utility> @@ -71,7 +72,9 @@ connect(m_pass, &Pass::finishedShow, this, &PasswordDialog::setPass); connect(m_pass, &Pass::processErrorExit, this, &PasswordDialog::onShowError); - connect(this, &PasswordDialog::accepted, this, &PasswordDialog::on_accepted); + connect(m_pass, &Pass::finishedInsert, this, + &PasswordDialog::onInsertFinished); + connect(m_pass, &Pass::insertFailed, this, &PasswordDialog::onInsertFailed); connect(this, &PasswordDialog::rejected, this, &PasswordDialog::on_rejected); if (!isNew) { @@ -127,10 +130,34 @@ ui->widget->setEnabled(true); } +void PasswordDialog::accept() { + if (m_saving) { + return; + } + save(); +} + +void PasswordDialog::reject() { + if (m_saving) { + const QMessageBox::StandardButton answer = QMessageBox::question( + this, tr("Still saving"), + tr("The entry is still being saved. Close the dialog anyway? If " + "saving then fails, what you typed is lost."), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + // The insert may have reported while the question was up. + if (answer != QMessageBox::Yes || !m_saving) { + return; + } + // Whatever the insert reports later, the main window shows it. + m_saving = false; + } + QDialog::reject(); +} + /** - * @brief PasswordDialog::on_accepted handle Ok click for QDialog + * @brief PasswordDialog::save insert the entry and wait for the result. */ -void PasswordDialog::on_accepted() { +void PasswordDialog::save() { // For an existing entry, refuse to save until its decrypted content has // loaded (Show is asynchronous). getPassword() always returns at least a // newline, so the previous isEmpty() check never fired and clicking OK early @@ -147,7 +174,60 @@ newValue += "\n"; } + setSaving(true); + // A refusal (an unusable or unsigned .gpg-id) comes through critical() + // before Insert() returns, with nothing started that could still report. + QString refusal; + const QMetaObject::Connection refused = + connect(m_pass, &Pass::critical, this, + [&refusal](const QString &, const QString &message) { + refusal = message; + }); m_pass->Insert(m_file, newValue, !m_isNew); + disconnect(refused); + if (m_saving && !refusal.isEmpty()) { + setSaving(false); + showNotSaved(refusal); + } +} + +void PasswordDialog::setSaving(bool saving) { + m_saving = saving; + setEditorEnabled(!saving); + // Cancel stays: a save that never reports (an unanswered PIN prompt, a + // queue held up by a hanging pull) must not leave quitting as the only + // way out. reject() asks first. + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!saving); + ui->statusLabel->setText(saving ? tr("Saving…") : QString()); +} + +void PasswordDialog::onInsertFinished() { + // Only one insert can be in flight while this modal dialog waits: an + // earlier dialog closed only once its own had reported. + if (!m_saving) { + return; + } + setSaving(false); + QDialog::accept(); +} + +void PasswordDialog::onInsertFailed(const QString &err, bool written) { + if (!m_saving) { + return; + } + setSaving(false); + // The entry is in the store and a later step (git add or commit) failed, + // which the main window reports. Kept open, a new entry could only be + // tried again under a name it now has. + if (m_isNew && written) { + QDialog::accept(); + return; + } + showNotSaved(err); +} + +void PasswordDialog::showNotSaved(const QString &err) { + ui->statusLabel->setText(tr("Not saved: %1").arg(err.trimmed())); } /** @@ -457,6 +537,10 @@ * @param templateName Name of template to apply. */ void PasswordDialog::applyTemplate(const QString &templateName) { + // Another template would rebuild the field rows under a pending insert. + if (m_saving) { + return; + } auto it = m_availableTemplates.constFind(templateName); if (it != m_availableTemplates.constEnd()) { m_currentTemplateName = templateName; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/QtPass-1.8.2/src/passworddialog.h new/QtPass-1.8.3/src/passworddialog.h --- old/QtPass-1.8.2/src/passworddialog.h 2026-09-24 23:00:10.000000000 +0200 +++ new/QtPass-1.8.3/src/passworddialog.h 2026-10-06 13:09:31.000000000 +0200 @@ -48,6 +48,25 @@ ~PasswordDialog() override; /** + * @brief OK: insert the entry and close as accepted only once the insert + * has succeeded. + * + * The insert is asynchronous. Until it reports back the dialog stays open + * with its fields locked; when it fails (a missing recipient key, an + * unusable `.gpg-id`) the fields come back with what was typed and the + * reason is shown, instead of the entry being lost with the closed dialog. + */ + void accept() override; + + /** + * @brief Cancel, Esc and the window's close button. While an insert is in + * flight they ask first: closing then means a failure can no longer hand + * back what was typed, but a save that never reports must not leave + * quitting as the only way out. + */ + void reject() override; + + /** * @brief Populate the dialog's password field with the given text. * @param password Password text to display. * @sa getPassword @@ -119,10 +138,22 @@ private slots: void on_checkBoxShow_stateChanged(int arg1); void on_createPasswordButton_clicked(); - void on_accepted(); void on_rejected(); /** + * @brief The insert started by accept() succeeded: close as accepted. + */ + void onInsertFinished(); + /** + * @brief The insert started by accept() failed: unlock the fields, which + * still hold what was typed, and show why. + * @param err The reason reported by the backend. + * @param written true when the entry was written and only a later step + * (git) failed; a new entry then closes the dialog. + */ + void onInsertFailed(const QString &err, bool written); + + /** * @brief Handle a process error while waiting for an entry's decrypt. * * When the asynchronous Show() fails, keep the dialog open and surface the @@ -149,6 +180,21 @@ */ void setEditorEnabled(bool enabled); /** + * @brief Start inserting what the dialog holds and wait for the result. + */ + void save(); + /** + * @brief Lock or unlock the dialog while an insert is in flight. + * @param saving true while waiting for the backend. + */ + void setSaving(bool saving); + /** + * @brief Say why the entry was not saved and let the user try again; the + * fields keep what was typed. + * @param err The reason reported by the backend. + */ + void showNotSaved(const QString &err); + /** * @brief Connect validation and normalisation to the OTP field, if present. * * Called whenever the field widgets are rebuilt, since both setTemplate() @@ -176,9 +222,11 @@ bool m_allFields{}; bool m_isNew{}; /// True once the existing entry's decrypted content has been loaded, so - /// on_accepted() can refuse to overwrite it with empty fields before the + /// save() can refuse to overwrite it with empty fields before the /// asynchronous Show completes. bool m_contentLoaded{}; + /// True from accept() starting an insert until the backend reports back. + bool m_saving{}; /// Last pwgen mode passed to usePwgen(), so the policy can be re-applied /// after setEditorEnabled() re-enables the character-set widgets. bool m_usePwgen{}; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/QtPass-1.8.2/src/realpass.cpp new/QtPass-1.8.3/src/realpass.cpp --- old/QtPass-1.8.2/src/realpass.cpp 2026-09-24 23:00:10.000000000 +0200 +++ new/QtPass-1.8.3/src/realpass.cpp 2026-10-06 13:09:31.000000000 +0200 @@ -77,6 +77,11 @@ * @brief RealPass::Insert pass insert */ void RealPass::Insert(QString file, QString newValue, bool overwrite) { + m_insertTarget = + QDir::cleanPath(QDir::isAbsolutePath(file) + ? file + ".gpg" + : QDir(m_settings.passStore).filePath(file) + ".gpg"); + m_insertTargetExisted = QFileInfo::exists(m_insertTarget); QStringList args = {"insert", "-m"}; if (overwrite) { args.append("-f"); @@ -85,6 +90,11 @@ executePass(PASS_INSERT, args, newValue); } +auto RealPass::insertWritten() const -> bool { + return !m_insertTarget.isEmpty() && !m_insertTargetExisted && + QFileInfo::exists(m_insertTarget); +} + /** * @brief RealPass::Remove pass remove wrapper */ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/QtPass-1.8.2/src/realpass.h new/QtPass-1.8.3/src/realpass.h --- old/QtPass-1.8.2/src/realpass.h 2026-09-24 23:00:10.000000000 +0200 +++ new/QtPass-1.8.3/src/realpass.h 2026-10-06 13:09:31.000000000 +0200 @@ -42,6 +42,18 @@ void passMoveOrCopy(PROCESS id, const QString &subcommand, const QString &src, const QString &dest, bool force); + /** + * @brief Whether the failed `pass insert` wrote its entry before its git + * step failed. pass does not say, so: the entry did not exist when the + * insert started and does now. + * @return true when a new entry appeared. + */ + [[nodiscard]] auto insertWritten() const -> bool override; + + /// The entry the last Insert() writes, and whether it existed beforehand. + QString m_insertTarget; + bool m_insertTargetExisted = false; + public: /** * @brief Construct a RealPass instance. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/QtPass-1.8.2/tests/auto/passworddialog/tst_passworddialog.cpp new/QtPass-1.8.3/tests/auto/passworddialog/tst_passworddialog.cpp --- old/QtPass-1.8.2/tests/auto/passworddialog/tst_passworddialog.cpp 2026-09-24 23:00:10.000000000 +0200 +++ new/QtPass-1.8.3/tests/auto/passworddialog/tst_passworddialog.cpp 2026-10-06 13:09:31.000000000 +0200 @@ -15,9 +15,11 @@ #include <QDialogButtonBox> #include <QLabel> #include <QLineEdit> +#include <QMessageBox> #include <QPlainTextEdit> #include <QPushButton> #include <QSignalSpy> +#include <QTimer> #include <QToolButton> #include <QtTest> @@ -45,7 +47,34 @@ void GitPush() override {} void Show(QString) override {} void OtpGenerate(QString) override {} - void Insert(QString, QString, bool) override {} + void Insert(QString file, QString, bool) override { + inserted = file; + // The backends refuse before starting anything through critical(), + // and report a started insert asynchronously. + if (!refuseWith.isEmpty()) { + emit critical(QStringLiteral("Can not edit"), refuseWith); + return; + } + if (holdResult) { + return; + } + if (!failWith.isEmpty()) { + QTimer::singleShot(0, this, [this, why = failWith] { + emit insertFailed(why, failWritten); + }); + return; + } + emit finishedInsert(QString(), QString()); + } + QString inserted; + /// Refuse the next Insert() synchronously with this reason. + QString refuseWith; + /// Fail the next Insert() asynchronously with this reason. + QString failWith; + /// Whether that failure says the entry was written (a git step failed). + bool failWritten = false; + /// Report nothing yet, as while gpg is still running. + bool holdResult = false; void Remove(QString, bool) override {} void Move(const QString, const QString, const bool) override {} void Copy(const QString, const QString, const bool) override {} @@ -72,6 +101,11 @@ void initTestCase() { isolateTestSettings(); } void existingEntryLocksEditorUntilContentLoads(); + void failedInsertKeepsWhatWasTyped(); + void refusedInsertKeepsWhatWasTyped(); + void savingCancelAsksBeforeClosing(); + void savingIgnoresATemplateSwitch(); + void newEntryWrittenBeforeAFailedGitStepCloses(); void contentLoadUnlocksEditorAndOk(); void decryptErrorKeepsDialogOpenWithReason(); void lateErrorAfterContentLoadIsIgnored(); @@ -169,5 +203,147 @@ QVERIFY2(pw->isEnabled(), "a new entry must be editable immediately"); } +/** + * @brief #1944: when encrypting fails (a missing recipient key, say) the + * dialog stays open with what was typed and says why; OK then tries + * again. It used to close before gpg ran, and the entry was lost. + */ +void tst_passworddialog::failedInsertKeepsWhatWasTyped() { + FakePass pass; + pass.failWith = QStringLiteral("gpg: [email protected]: No public key"); + PasswordDialog d(&pass, QtPassSettings::load(), QStringLiteral("mail"), true); + auto *password = d.findChild<QLineEdit *>(QStringLiteral("lineEditPassword")); + auto *status = d.findChild<QLabel *>(QStringLiteral("statusLabel")); + QVERIFY(password != nullptr && status != nullptr); + password->setText(QStringLiteral("s3cret")); + QSignalSpy accepted(&d, &QDialog::accepted); + + okButton(d)->click(); + QTRY_VERIFY2(status->text().contains(QStringLiteral("No public key")), + qPrintable("the reason must be shown: " + status->text())); + QVERIFY2(accepted.isEmpty(), "a failed save must not close the dialog"); + QCOMPARE(password->text(), QStringLiteral("s3cret")); + QVERIFY(password->isEnabled() && okButton(d)->isEnabled()); + + pass.failWith.clear(); + okButton(d)->click(); + QCOMPARE(accepted.size(), 1); + QCOMPARE(pass.inserted, QStringLiteral("mail")); +} + +/** + * @brief A refusal before anything starts (an unusable .gpg-id) keeps the + * dialog open the same way. + */ +void tst_passworddialog::refusedInsertKeepsWhatWasTyped() { + FakePass pass; + pass.refuseWith = QStringLiteral("Could not read encryption key to use"); + PasswordDialog d(&pass, QtPassSettings::load(), QStringLiteral("mail"), true); + auto *password = d.findChild<QLineEdit *>(QStringLiteral("lineEditPassword")); + password->setText(QStringLiteral("s3cret")); + QSignalSpy accepted(&d, &QDialog::accepted); + + okButton(d)->click(); + QVERIFY2(accepted.isEmpty(), "a refused save must not close the dialog"); + QVERIFY(d.findChild<QLabel *>(QStringLiteral("statusLabel")) + ->text() + .contains(QStringLiteral("encryption key"))); + QCOMPARE(password->text(), QStringLiteral("s3cret")); + QVERIFY(okButton(d)->isEnabled()); +} + +namespace { +/// Answer the next modal question box with @p button once it is up. +void answerNextQuestion(QMessageBox::StandardButton button, int tries = 50) { + QTimer::singleShot(10, [button, tries] { + auto *box = qobject_cast<QMessageBox *>(QApplication::activeModalWidget()); + if (box == nullptr) { + if (tries > 0) { + answerNextQuestion(button, tries - 1); + } + return; + } + box->button(button)->click(); + }); +} +} // namespace + +/** + * @brief A save that never reports (an unanswered PIN prompt, a queue held + * up by a hanging pull) must not trap the user: Cancel stays on while + * saving, and Cancel, Esc or closing ask first. No keeps waiting; Yes + * closes, and a result arriving later no longer touches the dialog. + */ +void tst_passworddialog::savingCancelAsksBeforeClosing() { + FakePass pass; + pass.holdResult = true; + PasswordDialog d(&pass, QtPassSettings::load(), QStringLiteral("mail"), true); + d.findChild<QLineEdit *>(QStringLiteral("lineEditPassword")) + ->setText(QStringLiteral("s3cret")); + d.show(); + QVERIFY(QTest::qWaitForWindowExposed(&d)); + QSignalSpy rejected(&d, &QDialog::rejected); + QSignalSpy accepted(&d, &QDialog::accepted); + auto *box = d.findChild<QDialogButtonBox *>(QStringLiteral("buttonBox")); + + okButton(d)->click(); + QVERIFY2(box->button(QDialogButtonBox::Cancel)->isEnabled(), + "Cancel is the way out of a save that never reports"); + answerNextQuestion(QMessageBox::No); + d.reject(); + QVERIFY2(rejected.isEmpty() && d.isVisible(), "No keeps waiting"); + answerNextQuestion(QMessageBox::No); + QTest::keyClick(&d, Qt::Key_Escape); + QVERIFY2(rejected.isEmpty() && d.isVisible(), "Esc asks the same"); + + answerNextQuestion(QMessageBox::Yes); + box->button(QDialogButtonBox::Cancel)->click(); + QCOMPARE(rejected.size(), 1); + emit pass.finishedInsert(QString(), QString()); + emit pass.insertFailed(QStringLiteral("late"), false); + QVERIFY2(accepted.isEmpty(), "a late result does not reopen or accept"); +} + +/** + * @brief Ctrl+T while an insert is pending would rebuild the field rows + * under it; the switch is ignored until the result. + */ +void tst_passworddialog::savingIgnoresATemplateSwitch() { + FakePass pass; + pass.holdResult = true; + PasswordDialog d(&pass, QtPassSettings::load(), QStringLiteral("mail"), true); + QHash<QString, QStringList> templates; + templates.insert(QStringLiteral("login"), {QStringLiteral("login")}); + templates.insert(QStringLiteral("card"), {QStringLiteral("number")}); + d.setAvailableTemplates(templates, QStringLiteral("login")); + auto *login = d.findChild<QLineEdit *>(QStringLiteral("login")); + QVERIFY(login != nullptr); + login->setText(QStringLiteral("bob")); + + okButton(d)->click(); + d.cycleTemplate(); + QVERIFY2(d.findChild<QLineEdit *>(QStringLiteral("login")) != nullptr, + "the typed field must survive a template switch while saving"); + emit pass.insertFailed(QStringLiteral("gpg: no public key"), false); + QCOMPARE(d.findChild<QLineEdit *>(QStringLiteral("login"))->text(), + QStringLiteral("bob")); +} + +/** + * @brief When the entry was written and only a later step failed (git add + * or commit), the dialog closes: the entry is saved, and the main + * window reports the git error. + */ +void tst_passworddialog::newEntryWrittenBeforeAFailedGitStepCloses() { + FakePass pass; + pass.failWith = QStringLiteral("fatal: unable to write new index file"); + pass.failWritten = true; + PasswordDialog d(&pass, QtPassSettings::load(), QStringLiteral("mail"), true); + QSignalSpy accepted(&d, &QDialog::accepted); + + okButton(d)->click(); + QTRY_COMPARE(accepted.size(), 1); +} + QTEST_MAIN(tst_passworddialog) #include "tst_passworddialog.moc" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/QtPass-1.8.2/tests/auto/util/tst_util.cpp new/QtPass-1.8.3/tests/auto/util/tst_util.cpp --- old/QtPass-1.8.2/tests/auto/util/tst_util.cpp 2026-09-24 23:00:10.000000000 +0200 +++ new/QtPass-1.8.3/tests/auto/util/tst_util.cpp 2026-10-06 13:09:31.000000000 +0200 @@ -101,6 +101,7 @@ Pass::finished(id, exitCode, out, err); } bool callCreateBackupCommit() { return createBackupCommit(); } + bool written() const { return insertWritten(); } }; template <typename T, void (*Setter)(const T &)> struct SettingGuard { @@ -249,6 +250,8 @@ void parseGrepOutputPlainTextHeaders(); // Pass::finished PASS_GREP exit-code handling void passFinishedGrepNoMatchEmitsEmpty(); + void passFinishedInsertErrorEmitsInsertFailed(); + void imitatePassInsertIsWrittenOnceGpgSucceeded(); void passFinishedGrepErrorEmitsProcessError(); void passFinishedGrepSuccessEmitsResults(); // ImitatePass::Grep / helpers @@ -345,6 +348,42 @@ } /** + * @brief Any failed insert reaches the password dialog through insertFailed + * (#1944), with the message processErrorExit carries; other commands + * failing do not. + */ +void tst_util::passFinishedInsertErrorEmitsInsertFailed() { + TestPass pass; + QSignalSpy failedSpy(&pass, &Pass::insertFailed); + QSignalSpy errSpy(&pass, &Pass::processErrorExit); + pass.callPassFinished(static_cast<int>(Enums::PASS_INSERT), 1, QString(), + QStringLiteral("fatal: unable to write index\n")); + QCOMPARE(failedSpy.count(), 1); + QCOMPARE(errSpy.count(), 1); + QCOMPARE(failedSpy.at(0).at(0).toString(), errSpy.at(0).at(1).toString()); + QVERIFY2(!failedSpy.at(0).at(1).toBool(), "no insert wrote anything"); + + pass.callPassFinished(static_cast<int>(Enums::GIT_PUSH), 1, QString(), + QStringLiteral("rejected\n")); + QCOMPARE(failedSpy.count(), 1); +} + +/** + * @brief ImitatePass's gpg writes the entry itself: once that step succeeded + * a failing git step no longer means nothing was saved. + */ +void tst_util::imitatePassInsertIsWrittenOnceGpgSucceeded() { + TestPass pass; + QVERIFY(!pass.written()); + pass.callFinished(static_cast<int>(Enums::PASS_INSERT), 2, QString(), + QStringLiteral("gpg: encryption failed\n")); + QVERIFY2(!pass.written(), "a failed gpg wrote nothing"); + pass.callFinished(static_cast<int>(Enums::PASS_INSERT), 0, QString(), + QString()); + QVERIFY(pass.written()); +} + +/** * @brief tst_util::normalizeFolderPath test to check correct working * of Util::normalizeFolderPath the paths should always end with '/' */ ++++++ _service ++++++ --- /var/tmp/diff_new_pack.Mv9VMS/_old 2026-10-07 19:32:43.248930163 +0200 +++ /var/tmp/diff_new_pack.Mv9VMS/_new 2026-10-07 19:32:43.252930330 +0200 @@ -6,7 +6,7 @@ <param name="exclude">.git</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> - <param name="revision">v1.8.2</param> + <param name="revision">v1.8.3</param> <param name="changesgenerate">enable</param> </service> <service name="recompress" mode="manual"> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.Mv9VMS/_old 2026-10-07 19:32:43.295932126 +0200 +++ /var/tmp/diff_new_pack.Mv9VMS/_new 2026-10-07 19:32:43.299932293 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/IJHack/QtPass.git</param> - <param name="changesrevision">71122182ddaef33d83c60868a7baff4a85442f1a</param></service></servicedata> + <param name="changesrevision">dadb11b7638603421acedea6f0e0cf034e0a93df</param></service></servicedata> (No newline at EOF)
