Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package duplicity for openSUSE:Factory 
checked in at 2026-01-09 17:04:31
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/duplicity (Old)
 and      /work/SRC/openSUSE:Factory/.duplicity.new.1928 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "duplicity"

Fri Jan  9 17:04:31 2026 rev:88 rq:1326307 version:3.0.7

Changes:
--------
--- /work/SRC/openSUSE:Factory/duplicity/duplicity.changes      2025-12-08 
11:54:36.099114827 +0100
+++ /work/SRC/openSUSE:Factory/.duplicity.new.1928/duplicity.changes    
2026-01-09 17:06:21.659135275 +0100
@@ -1,0 +2,12 @@
+Wed Dec 31 15:34:59 UTC 2025 - Michael Gorse <[email protected]>
+
+- Update to version 3.0.7:
+  * fix: replace custom deltree with built-in shutil.rmtree.
+  * fix: delete duplicate code in DirDelta.
+  * fix: webdavs with "--concurrency 1" failed because of missing
+    auth header.
+  * fix: disable s3 checksum workaround, warn only...
+  * fix: --log-timestamp no longer working.
+  * fix: Combined fix to related issues 912 and 914.
+
+-------------------------------------------------------------------

Old:
----
  duplicity-rel.3.0.6.3.tar.bz2

New:
----
  duplicity-rel.3.0.7.tar.bz2

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ duplicity.spec ++++++
--- /var/tmp/diff_new_pack.yNaeJw/_old  2026-01-09 17:06:22.583173577 +0100
+++ /var/tmp/diff_new_pack.yNaeJw/_new  2026-01-09 17:06:22.587173742 +0100
@@ -26,7 +26,7 @@
 %define _python3_version %{?python311_version}
 %endif
 Name:           duplicity
-Version:        3.0.6.3
+Version:        3.0.7
 Release:        0
 Summary:        Encrypted bandwidth-efficient backup using the rsync algorithm
 License:        GPL-3.0-or-later

++++++ duplicity-rel.3.0.6.3.tar.bz2 -> duplicity-rel.3.0.7.tar.bz2 ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/duplicity-rel.3.0.6.3/CHANGELOG.md 
new/duplicity-rel.3.0.7/CHANGELOG.md
--- old/duplicity-rel.3.0.6.3/CHANGELOG.md      2025-12-05 17:21:40.000000000 
+0100
+++ new/duplicity-rel.3.0.7/CHANGELOG.md        2025-12-31 13:07:54.000000000 
+0100
@@ -1,9 +1,19 @@
 
-(Unreleased) / 2025-12-05
+(Unreleased) / 2025-12-31
 =========================
 
 
 
+rel.3.0.7 / 2025-12-31
+======================
+
+ * ad175c22:fix: replace custom deltree with built-in shutil.rmtree.
+ * 101be7fd:fix: delete duplicate code in DirDelta.
+ * c0d3e72d:fix: webdavs with "--concurrency 1" failed because of missing auth 
header
+ * a08f8bef:fix: disable s3 checksum workaround, warn only...
+ * f7055a7a:fix: --log-timestamp no longer working.
+ * ac163d5e:fix: Combined fix to related issues 912 and 914
+
 rel.3.0.6.3 / 2025-12-05
 ========================
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/duplicity-rel.3.0.6.3/duplicity/__init__.py 
new/duplicity-rel.3.0.7/duplicity/__init__.py
--- old/duplicity-rel.3.0.6.3/duplicity/__init__.py     2025-12-05 
17:21:40.000000000 +0100
+++ new/duplicity-rel.3.0.7/duplicity/__init__.py       2025-12-31 
13:07:54.000000000 +0100
@@ -21,7 +21,7 @@
 
 import gettext
 
-__version__: str = "3.0.6.3"
-__reldate__: str = "December 05, 2025"
+__version__: str = "3.0.7"
+__reldate__: str = "December 31, 2025"
 
 gettext.install("duplicity", names=["ngettext"])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/duplicity-rel.3.0.6.3/duplicity/backends/s3_boto3_backend.py 
new/duplicity-rel.3.0.7/duplicity/backends/s3_boto3_backend.py
--- old/duplicity-rel.3.0.6.3/duplicity/backends/s3_boto3_backend.py    
2025-12-05 17:21:40.000000000 +0100
+++ new/duplicity-rel.3.0.7/duplicity/backends/s3_boto3_backend.py      
2025-12-31 13:07:54.000000000 +0100
@@ -74,19 +74,6 @@
         from boto3.s3.transfer import S3UploadFailedError, TransferConfig
         from botocore.exceptions import ClientError
 
-        if not (boto3.__version__ < "1.36.0" and botocore.__version__ < 
"1.36.0"):
-            # TODO: remove this workaround when issue #870 is fixed.
-            # https://github.com/boto/boto3/issues/2913
-            log.Warn(
-                "WARNING: Using boto3 >= 1,36.0 may result in errors, so we 
qre applying\n"
-                "the workaround for 
https://gitlab.com/duplicity/duplicity/-/issues/870\n";
-                "    export AWS_REQUEST_CHECKSUM_CALCULATION=when_required\n"
-                "    export AWS_RESPONSE_CHECKSUM_VALIDATION=when_required\n"
-                "NOTE: This workaround is temporary and will be removed when 
issue is fixed.\n."
-            )
-            os.environ["AWS_REQUEST_CHECKSUM_CALCULATION"] = "when_required"
-            os.environ["AWS_RESPONSE_CHECKSUM_VALIDATION"] = "when_required"
-
         duplicity.backend.Backend.__init__(self, parsed_url)
 
         # This folds the null prefix and all null parts, which means that:
@@ -109,6 +96,25 @@
         self.bucket = None
         self.tracker = UploadProgressTracker()
 
+        if not (boto3.__version__ < "1.36.0" and botocore.__version__ < 
"1.36.0"):
+            # this is an issue with 3rd party s3 implementations only
+            # likely when an endpoint is given that resides not under 
amazonaws.com
+            # in time that workaround will probably not be needed anymore
+            # https://github.com/boto/boto3/issues/2913
+            import re
+
+            if config.s3_endpoint_url and not re.match(
+                pattern="(?i).*\\.amazonaws\\.com(/+)?$", 
string=config.s3_endpoint_url
+            ):
+                log.Warn(
+                    "WARNING: Using boto3 >= 1,36.0 with non-amazon s3 
services"
+                    " may result in checksum errors."
+                    " a workaround is to set the following env vars\n\n"
+                    "    export 
AWS_REQUEST_CHECKSUM_CALCULATION=when_required\n"
+                    "    export 
AWS_RESPONSE_CHECKSUM_VALIDATION=when_required\n\n"
+                    "see https://gitlab.com/duplicity/duplicity/-/issues/870 
for details."
+                )
+
     def reset_connection(self):
         self.bucket = None
         self.s3 = boto3.resource(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/duplicity-rel.3.0.6.3/duplicity/backends/webdavbackend.py 
new/duplicity-rel.3.0.7/duplicity/backends/webdavbackend.py
--- old/duplicity-rel.3.0.6.3/duplicity/backends/webdavbackend.py       
2025-12-05 17:21:40.000000000 +0100
+++ new/duplicity-rel.3.0.7/duplicity/backends/webdavbackend.py 2025-12-31 
13:07:54.000000000 +0100
@@ -194,9 +194,10 @@
         if self.username or self.password:
             # Workaround cpython http.client issue
             # https://github.com/python/cpython/issues/70107
-            self.conn.request("OPTIONS", self.directory, None)
-            response = self.conn.getresponse()
-            response.read()
+            # PUT may not return 401 when ran without basic-auth but throw 
SSL-EOF-Error or hang
+            # as a workaround we run an OPTIONS request that adds auth if 
needed and creates
+            # an authenticated connection to (re)use
+            response = self.request("OPTIONS", self.directory, None)
             response.close()
 
     def _close(self):
@@ -226,6 +227,8 @@
 
         if self.digest_challenge is not None:
             self.headers["Authorization"] = self.get_digest_authorization(path)
+        elif self.username or self.password:
+            self.headers["Authorization"] = self.get_basic_authorization()
 
         log.Debug(_("WebDAV %s %s request with headers: %s ") % (method, 
quoted_path, munge_headers(self.headers)))
         log.Debug(_("WebDAV data length: %s ") % sys.getsizeof(data))
@@ -245,6 +248,7 @@
                 return self.request(method, self.directory, data, redirected + 
1)
             else:
                 raise FatalBackendException(_("WebDAV missing location header 
in redirect response."))
+        # mainly for digest-auth to recalculate with response values
         elif response.status == 401:
             response.read()
             response.close()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/duplicity-rel.3.0.6.3/duplicity/cli_data.py 
new/duplicity-rel.3.0.7/duplicity/cli_data.py
--- old/duplicity-rel.3.0.6.3/duplicity/cli_data.py     2025-12-05 
17:21:40.000000000 +0100
+++ new/duplicity-rel.3.0.7/duplicity/cli_data.py       2025-12-31 
13:07:54.000000000 +0100
@@ -446,9 +446,9 @@
         type=set_log_file,
         help="Logging filename to use",
     ),
-    # log_timestamp is directly applied in SetLogTimestampAction(), not saved 
in config
+    # log_timestamp is directly applied in set_log_timestamp(), not saved in 
config
     log_timestamp=dict(
-        dest="",
+        nargs=0,
         action=SetLogTimestampAction,
         help="Whether to include timestamp and level in log",
         default=dflt(False),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/duplicity-rel.3.0.6.3/duplicity/cli_util.py 
new/duplicity-rel.3.0.7/duplicity/cli_util.py
--- old/duplicity-rel.3.0.6.3/duplicity/cli_util.py     2025-12-05 
17:21:40.000000000 +0100
+++ new/duplicity-rel.3.0.7/duplicity/cli_util.py       2025-12-31 
13:07:54.000000000 +0100
@@ -160,7 +160,7 @@
         super().__init__(option_strings, dest, **kwargs)
 
     def __call__(self, parser, namespace, values, option_string=None):
-        log._log_timestamp = True
+        log.add_timestamp()
 
 
 def _check_int(val):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/duplicity-rel.3.0.6.3/duplicity/diffdir.py 
new/duplicity-rel.3.0.7/duplicity/diffdir.py
--- old/duplicity-rel.3.0.6.3/duplicity/diffdir.py      2025-12-05 
17:21:40.000000000 +0100
+++ new/duplicity-rel.3.0.7/duplicity/diffdir.py        2025-12-31 
13:07:54.000000000 +0100
@@ -37,7 +37,7 @@
 from duplicity import util
 from duplicity.path import *  # pylint: 
disable=unused-wildcard-import,redefined-builtin
 
-# A StatsObj will be written to this from DirDelta and DirDelta_WriteSig.
+# A StatsObj will be written to this from DirDelta_WriteSig.
 stats = None
 tracker = None
 
@@ -55,7 +55,7 @@
     will be easy to split up the tar and make the volumes the same
     sizes).
     """
-    return DirDelta(path_iter, io.StringIO(""))
+    return DirDelta_WriteSig(path_iter, io.StringIO(""), None)
 
 
 def DirFull_WriteSig(path_iter, sig_outfp):
@@ -65,26 +65,6 @@
     return DirDelta_WriteSig(path_iter, io.StringIO(""), sig_outfp)
 
 
-def DirDelta(path_iter, dirsig_fileobj_list):
-    """
-    Produce tarblock diff given dirsig_fileobj_list and pathiter
-
-    dirsig_fileobj_list should either be a tar fileobj or a list of
-    those, sorted so the most recent is last.
-    """
-    global stats
-    stats = statistics.StatsDeltaProcess()
-    if isinstance(dirsig_fileobj_list, list):
-        sig_iter = combine_path_iters([sigtar2path_iter(x) for x in 
dirsig_fileobj_list])
-    else:
-        sig_iter = sigtar2path_iter(dirsig_fileobj_list)
-    delta_iter = get_delta_iter(path_iter, sig_iter)
-    if config.dry_run or (config.progress and not 
progress.tracker.has_collected_evidence()):
-        return DummyBlockIter(delta_iter)
-    else:
-        return DeltaTarBlockIter(delta_iter)
-
-
 def delta_iter_error_handler(exc, new_path, sig_path, sig_tar=None):  # 
pylint: disable=unused-argument
     """
     Called by get_delta_iter, report error in getting delta
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/duplicity-rel.3.0.6.3/duplicity/dup_main.py 
new/duplicity-rel.3.0.7/duplicity/dup_main.py
--- old/duplicity-rel.3.0.6.3/duplicity/dup_main.py     2025-12-05 
17:21:40.000000000 +0100
+++ new/duplicity-rel.3.0.7/duplicity/dup_main.py       2025-12-31 
13:07:54.000000000 +0100
@@ -132,24 +132,31 @@
         log.Notice(_("Reuse configured SIGN_PASSPHRASE as PASSPHRASE"))
         return os.environ["SIGN_PASSPHRASE"]
 
-    # Not in the environment, check if encryption passphrase is needed
+    # no passphrase if --no-encryption or --use-agent
+    if not config.encryption or config.use_agent:
+        return ""
+
+    # no passphrase if --passphrase* in --gpg-options
+    if "--passphrase" in config.gpg_options:
+        return ""
+
+    # Check if encryption passphrase is needed
     asymmetric = False
     need_passphrase = False
     profile = config.gpg_profile
     encrypt_keys = profile.recipients + profile.hidden_recipients
     if profile.sign_key:
         encrypt_keys.append(profile.sign_key)
-    if encrypt_keys:
+    if encrypt_keys and config.check_remote:
         asymmetric = True
         for key in encrypt_keys:
-            if util.key_needs_passphrase(key):
+            if util.key_needs_passphrase(config.gpg_binary, key):
                 log.Notice(f"Key {key} needs passphrase.")
                 need_passphrase = True
                 break
         else:
             log.Notice("No encryption keys need passphrase.")
     else:
-        symmetric = True
         need_passphrase = True
         log.Notice("No encryption keys configured.")
 
@@ -758,7 +765,7 @@
     if config.progress:
         progress.tracker = progress.ProgressTracker()
         # Fake a backup to compute total of moving bytes
-        tarblock_iter = diffdir.DirDelta(config.select, 
sig_chain.get_fileobjs())
+        tarblock_iter = diffdir.DirDelta_WriteSig(config.select, 
sig_chain.get_fileobjs(), None)
         dummy_backup(tarblock_iter)
         # Store computed stats to compute progress later
         progress.tracker.set_evidence(diffdir.stats, False)
@@ -768,7 +775,7 @@
         progress.progress_thread = progress.LogProgressThread()
 
     if config.dry_run:
-        tarblock_iter = diffdir.DirDelta(config.select, 
sig_chain.get_fileobjs())
+        tarblock_iter = diffdir.DirDelta_WriteSig(config.select, 
sig_chain.get_fileobjs(), None)
         bytes_written = dummy_backup(tarblock_iter)
     else:
         new_sig_outfp = get_sig_fileobj("new-sig")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/duplicity-rel.3.0.6.3/duplicity/log.py 
new/duplicity-rel.3.0.7/duplicity/log.py
--- old/duplicity-rel.3.0.6.3/duplicity/log.py  2025-12-05 17:21:40.000000000 
+0100
+++ new/duplicity-rel.3.0.7/duplicity/log.py    2025-12-31 13:07:54.000000000 
+0100
@@ -40,7 +40,6 @@
 PREFIX = ""
 
 _logger = None
-_log_timestamp = False
 
 
 def DupToLoggerLevel(verb):
@@ -321,7 +320,6 @@
     Initialize logging
     """
     global _logger
-    global _log_timestamp
     if _logger:
         return
 
@@ -333,18 +331,12 @@
 
     # stdout and stderr are for different logging levels
     outHandler = logging.StreamHandler(sys.stdout)
-    if _log_timestamp:
-        outHandler.setFormatter(DetailFormatter())
-    else:
-        outHandler.setFormatter(PrettyProgressFormatter())
+    outHandler.setFormatter(PrettyProgressFormatter())
     outHandler.addFilter(OutFilter())
     _logger.addHandler(outHandler)
 
     errHandler = logging.StreamHandler(sys.stderr)
-    if _log_timestamp:
-        errHandler.setFormatter(DetailFormatter())
-    else:
-        errHandler.setFormatter(PrettyProgressFormatter())
+    errHandler.setFormatter(PrettyProgressFormatter())
     errHandler.addFilter(ErrFilter())
     _logger.addHandler(errHandler)
 
@@ -388,7 +380,7 @@
         # standard 'levelname'.  This is because the standard 'levelname' can
         # be adjusted by any library anywhere in our stack without us knowing.
         # But we control 'levelName'.
-        logging.Formatter.__init__(self, "%(asctime)s %(levelName)s 
%(message)s")
+        logging.Formatter.__init__(self, "%(asctime)s %(levelName)-6s 
%(message)s")
 
     def format(self, record):
         s = logging.Formatter.format(self, record)
@@ -452,6 +444,28 @@
     _logger.addHandler(handler)
 
 
+def add_timestamp():
+    """
+    Add timestamp to logs written
+    """
+    global _logger
+
+    # remove all handlers
+    for handler in _logger.handlers[:]:
+        _logger.removeHandler(handler)
+
+    # stdout and stderr are for different logging levels
+    outHandler = logging.StreamHandler(sys.stdout)
+    outHandler.setFormatter(DetailFormatter())
+    outHandler.addFilter(OutFilter())
+    _logger.addHandler(outHandler)
+
+    errHandler = logging.StreamHandler(sys.stderr)
+    errHandler.setFormatter(DetailFormatter())
+    errHandler.addFilter(ErrFilter())
+    _logger.addHandler(errHandler)
+
+
 def setverbosity(verb):
     """
     Set the verbosity level.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/duplicity-rel.3.0.6.3/duplicity/path.py 
new/duplicity-rel.3.0.7/duplicity/path.py
--- old/duplicity-rel.3.0.6.3/duplicity/path.py 2025-12-05 17:21:40.000000000 
+0100
+++ new/duplicity-rel.3.0.7/duplicity/path.py   2025-12-31 13:07:54.000000000 
+0100
@@ -623,13 +623,8 @@
 
     def deltree(self):
         """Remove self by recursively deleting files under it"""
-        from duplicity import selection  # TODO: avoid circ. dep. issue
-
         log.Debug(_("Deleting tree %s") % self.uc_name)
-        itr = IterTreeReducer(PathDeleter, [])
-        for path in selection.Select(self).set_iter():
-            itr(path.index, path)
-        itr.Finish()
+        shutil.rmtree(self.name)
         self.setdata()
 
     def get_parent_dir(self):
@@ -807,19 +802,3 @@
                 return gpg.GPGFile(True, self, gpg_profile)
         else:
             return self.open(mode)
-
-
-class PathDeleter(ITRBranch):
-    """Delete a directory.  Called by Path.deltree"""
-
-    def start_process(self, index, path):  # pylint: disable=unused-argument
-        self.path = path
-
-    def end_process(self):
-        self.path.delete()
-
-    def can_fast_process(self, index, path):  # pylint: disable=unused-argument
-        return not path.isdir()
-
-    def fast_process(self, index, path):  # pylint: disable=unused-argument
-        path.delete()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/duplicity-rel.3.0.6.3/duplicity/util.py 
new/duplicity-rel.3.0.7/duplicity/util.py
--- old/duplicity-rel.3.0.6.3/duplicity/util.py 2025-12-05 17:21:40.000000000 
+0100
+++ new/duplicity-rel.3.0.7/duplicity/util.py   2025-12-31 13:07:54.000000000 
+0100
@@ -27,11 +27,13 @@
 import csv
 import errno
 import json
+import locale
 import multiprocessing
 import os
 import socket
 import sys
 import traceback
+from contextlib import contextmanager
 from io import StringIO
 
 import fasteners
@@ -203,28 +205,93 @@
             pass
 
 
-def key_needs_passphrase(key):
+def key_needs_passphrase(gpgbin, key, logfile=None):
     """
-    Check if a key needs a passphrase.
+    Determine whether a GnuPG key requires a passphrase.
+
+    This helper invokes the specified GnuPG frontend in a non‑destructive
+    way to discover if the secret key is protected by a passphrase. It uses
+    `pexpect` to spawn the command and watch for prompts or agent errors,
+    never changing the key material itself.
+
+    How it works
+    - Runs: ``<gpgbin> --pinentry-mode cancel --dry-run --change-passphrase 
<key>``
+      with a C UTF‑8 locale to ensure predictable output.
+    - Interprets the interaction:
+        - If the process reaches EOF without a passphrase prompt, the key is
+          considered not to need a passphrase.
+        - If a passphrase prompt appears (matches ``passphrase.*:``), the key
+          is considered to need a passphrase.
+        - If ``gpg-agent`` fails to start or ignores an inquiry, we log an
+          error and return ``None`` to signal an indeterminate result.
+
+    Parameters
+    - gpgbin: str
+        The GnuPG command to execute, e.g. ``"gpg"`` or ``"gpgsm"``.
+    - key: str
+        The key identifier understood by the given binary. Examples:
+        - For ``gpg`` (OpenPGP): a key ID or fingerprint, e.g. ``"56538CCF"``.
+        - For ``gpgsm`` (S/MIME): a certificate keyref, e.g.
+          ``"\\&165F2FB4F58D..."``.
+    - logfile: a file-like object or ``None``
+        If provided, raw pexpect I/O is mirrored to this stream for debugging
+        (e.g. ``sys.stdout``). Defaults to ``None``.
+
+    Returns
+    - ``True``  if the key requires a passphrase.
+    - ``False`` if the key does not require a passphrase.
+    - ``None``  if the status cannot be determined due to a runtime error
+      (e.g., agent failed to start or pexpect raised an exception).
+
+    Notes
+    - The check is read‑only: ``--dry-run`` and ``--pinentry-mode cancel`` are
+      used to avoid modifying the key or prompting the user.
+    - Environment variables ``LANG`` and ``LC_ALL`` are forced to ``C.utf8``
+      to make output matching stable across locales.
+    - For end‑to‑end manual verification with the repository’s test keyring,
+      see ``testing/manual/needspass.py``.
     """
+
+    environ = {**os.environ, "LANG": "C.utf8", "LC_ALL": "C.utf8"}
+    cmd = f"{gpgbin} --pinentry-mode cancel --dry-run --change-passphrase 
{key} "
+
+    log.Debug(f"{cmd=}")
+
     try:
-        child = pexpect.spawn("gpg", f"--pinentry-mode=loopback --dry-run 
--passwd {key}".split())
-    except Exception:
-        log.FatalError(f"Exception spawning gpg while checking if passphrase 
needed for key: {key}")
+        child = pexpect.spawn(cmd, encoding="utf-8", env=environ)
+        child.logfile = logfile
+    except pexpect.ExceptionPexpect as e:
+        log.Error(f"An unexpected error occurred: {e}")
+        return None
 
     try:
-        got = child.expect(["passphrase.*:", pexpect.EOF])
-    except Exception:
-        log.FatalError(f"Exception while checking if passphrase needed for 
key: {key}: {str(child)}")
+        got = child.expect(
+            [
+                pexpect.EOF,
+                "passphrase.*:",
+                "failed to start gpg-agent",
+                "ignoring gpg-agent inquiry",
+            ]
+        )
+    except pexpect.ExceptionPexpect as e:
+        log.Error(f"Exception while checking if passphrase needed for: 
{key}:\n{e}")
+        return None
+
+    child.close()
+    log.Debug(f"{child.exitstatus=}, {child.signalstatus=}, {got=}, 
{child.after=}")
 
     if got == 0:
-        log.Debug(f"Key {key} needs passphrase")
-        child.close()
-        return True
-    elif got == 1:
         log.Debug(f"Key {key} does not need passphrase")
         return False
-    return None
+    elif got == 1:
+        log.Debug(f"Key {key} needs passphrase")
+        return True
+    elif got == 2:
+        log.Error(f"gpg-agent failed to start.")
+        return None
+    elif got == 3:
+        log.Error(f"gpg-agent failed inquiry ignored.")
+        return None
 
 
 def copyfileobj(infp, outfp, byte_count=-1):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/duplicity-rel.3.0.6.3/man/duplicity.1 
new/duplicity-rel.3.0.7/man/duplicity.1
--- old/duplicity-rel.3.0.6.3/man/duplicity.1   2025-12-05 17:21:40.000000000 
+0100
+++ new/duplicity-rel.3.0.7/man/duplicity.1     2025-12-31 13:07:54.000000000 
+0100
@@ -1,4 +1,4 @@
-.TH DUPLICITY 1 "December 05, 2025" "Version 3.0.6.3" "User Manuals" \"  -*- 
nroff -*-
+.TH DUPLICITY 1 "December 31, 2025" "Version 3.0.7" "User Manuals" \"  -*- 
nroff -*-
 .\" disable justification (adjust text to left margin only)
 .\" command line examples stay readable through that
 .ad l
@@ -498,23 +498,6 @@
 section for more information.
 
 .TP
-.BI "--files-from " filename
-Read a list of files to backup from filename rather than searching the entire
-backup source directory. Operation is otherwise normal, just on the specified
-subset of the backup source directory.
-
-Files must be specified one per line and relative to the backup source
-directory. Any absolute paths will raise an error. All characters per line are
-significant and treated as part of the path, including leading and trailing
-whitespace. Lines are separated by newlines or nulls, depending on whether the
-.B "--null-separator"
-switch was given.
-
-It is not necessary to include the parent directory of listed files, their
-inclusion is implied. However, the content of any explicitly listed directories
-is not implied. All required files must be listed when this option is used.
-
-.TP
 .BI "--file-prefix " prefix
 .PD 0
 .TP
@@ -535,12 +518,21 @@
 .B "A NOTE ON FILENAME PREFIXES"
 
 .TP
-.BI "--path-to-restore " path
-This option may be given in restore mode, causing only
-.I path
-to be restored instead of the entire contents of the backup archive.
-.I path
-should be given relative to the root of the directory backed up.
+.BI "--files-from " filename
+Read a list of files to backup from filename rather than searching the entire
+backup source directory. Operation is otherwise normal, just on the specified
+subset of the backup source directory.
+
+Files must be specified one per line and relative to the backup source
+directory. Any absolute paths will raise an error. All characters per line are
+significant and treated as part of the path, including leading and trailing
+whitespace. Lines are separated by newlines or nulls, depending on whether the
+.B "--null-separator"
+switch was given.
+
+It is not necessary to include the parent directory of listed files, their
+inclusion is implied. However, the content of any explicitly listed directories
+is not implied. All required files must be listed when this option is used.
 
 .TP
 .BI --filter-globbing
@@ -573,15 +565,6 @@
 section for more information.
 
 .TP
-.BI "--full-if-older-than " time
-Perform a full backup if an incremental backup is requested, but the
-latest full backup in the collection is older than the given
-.IR time .
-See the
-.B TIME FORMATS
-section for more information.
-
-.TP
 .BI --force
 Proceed even if data loss might result.  Duplicity will let the user
 know when this option is required.
@@ -597,6 +580,15 @@
 Use regular (PORT) data connections.
 
 .TP
+.BI "--full-if-older-than " time
+Perform a full backup if an incremental backup is requested, but the
+latest full backup in the collection is older than the given
+.IR time .
+See the
+.B TIME FORMATS
+section for more information.
+
+.TP
 .BI --gio
 Use the GIO backend and interpret any URLs as GIO would.
 
@@ -863,6 +855,14 @@
 Number of Par2 volumes to create (default 1).
 
 .TP
+.BI "--path-to-restore " path
+This option may be given in restore mode, causing only
+.I path
+to be restored instead of the entire contents of the backup archive.
+.I path
+should be given relative to the root of the directory backed up.
+
+.TP
 .BI --progress
 When selected, duplicity will output the current upload progress and estimated
 upload time. To annotate changes, it will perform a first dry-run before a full
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/duplicity-rel.3.0.6.3/pyproject.toml 
new/duplicity-rel.3.0.7/pyproject.toml
--- old/duplicity-rel.3.0.6.3/pyproject.toml    2025-12-05 17:21:40.000000000 
+0100
+++ new/duplicity-rel.3.0.7/pyproject.toml      2025-12-31 13:07:54.000000000 
+0100
@@ -1,6 +1,6 @@
 [project]
 name = "duplicity"
-version = "3.0.6.3"
+version = "3.0.7"
 dynamic = ["dependencies"]
 description = "Encrypted backup using rsync algorithm"
 authors = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/duplicity-rel.3.0.6.3/setup.py 
new/duplicity-rel.3.0.7/setup.py
--- old/duplicity-rel.3.0.6.3/setup.py  2025-12-05 17:21:40.000000000 +0100
+++ new/duplicity-rel.3.0.7/setup.py    2025-12-31 13:07:54.000000000 +0100
@@ -42,7 +42,7 @@
     print("Sorry, duplicity requires version 3.9 thru 3.14 of Python.", 
file=sys.stderr)
     sys.exit(1)
 
-Version: str = "3.0.6.3"
+Version: str = "3.0.7"
 
 # READTHEDOCS uses setup.py sdist but can't handle extensions
 ext_modules = list()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/duplicity-rel.3.0.6.3/snap/snapcraft.yaml 
new/duplicity-rel.3.0.7/snap/snapcraft.yaml
--- old/duplicity-rel.3.0.6.3/snap/snapcraft.yaml       2025-12-05 
17:21:40.000000000 +0100
+++ new/duplicity-rel.3.0.7/snap/snapcraft.yaml 2025-12-31 13:07:54.000000000 
+0100
@@ -1,5 +1,5 @@
 name: duplicity
-version: 3.0.6.3
+version: 3.0.7
 license: GPL-2.0
 summary: Efficient, encrypted backup to local or remote hosts
 description: |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/duplicity-rel.3.0.6.3/testing/functional/test_regression.py 
new/duplicity-rel.3.0.7/testing/functional/test_regression.py
--- old/duplicity-rel.3.0.6.3/testing/functional/test_regression.py     
2025-12-05 17:21:40.000000000 +0100
+++ new/duplicity-rel.3.0.7/testing/functional/test_regression.py       
2025-12-31 13:07:54.000000000 +0100
@@ -126,6 +126,7 @@
             ]
         )
 
+    @unittest.skipIf(os.path.exists("/.dockerenv"), "Won't work on docker")
     def test_issue908(self):
         """
         Test issue 908 - gpg: public key decryption failed: No passphrase 
given (3.0.6.2)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/duplicity-rel.3.0.6.3/testing/functional/test_restart.py 
new/duplicity-rel.3.0.7/testing/functional/test_restart.py
--- old/duplicity-rel.3.0.6.3/testing/functional/test_restart.py        
2025-12-05 17:21:40.000000000 +0100
+++ new/duplicity-rel.3.0.7/testing/functional/test_restart.py  2025-12-31 
13:07:54.000000000 +0100
@@ -88,6 +88,7 @@
         self.backup("full", f"{_runtest_dir}/testfiles/largefiles")
         self.verify(f"{_runtest_dir}/testfiles/largefiles")
 
+    @unittest.skipIf(os.path.exists("/.dockerenv"), "Won't work on docker")
     def test_restart_encrypt_without_password(self):
         """
         Test that we can successfully restart a encrypt-key-only backup without
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/duplicity-rel.3.0.6.3/testing/manual/needspass.py 
new/duplicity-rel.3.0.7/testing/manual/needspass.py
--- old/duplicity-rel.3.0.6.3/testing/manual/needspass.py       1970-01-01 
01:00:00.000000000 +0100
+++ new/duplicity-rel.3.0.7/testing/manual/needspass.py 2025-12-31 
13:07:54.000000000 +0100
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+"""
+Manual helper to verify whether specific GnuPG keys require a passphrase.
+
+What this script does
+---------------------
+- Changes to the project root and sets `GNUPGHOME` to use the test keyring in
+  `testing/gnupg`.
+- Calls `duplicity.util.key_needs_passphrase(gpgbin, key)` for a small set of
+  known test keys with both `gpg` and `gpgsm` and reports results.
+- Prints PASS/FAIL per key and exits with the number of failures as the status
+  code (0 means all checks passed).
+
+How to run
+----------
+- From the project root:
+    - `python3 testing/manual/needspass.py`
+
+Optional debugging
+------------------
+- To see the interaction with `gpg-agent` via `pexpect`, set 
`logfile=sys.stdout`
+  where indicated in the code (search for the comment “set 
logfile=sys.stdout”).
+
+Prerequisites
+-------------
+- `gpg`, `gpgsm`, and a functioning `gpg-agent` available in PATH.
+- No additional arguments are needed; the script is self‑contained for the
+  repository’s test keyring.
+
+Notes
+-----
+- The expected outcomes for the embedded test keys are specified in
+  `test_keys` below; modify or extend this list if you need to try other keys.
+"""
+
+import os
+import sys
+
+os.chdir(os.path.dirname(__file__) + "/../..")
+os.environ["GNUPGHOME"] = "testing/gnupg"
+
+from duplicity import log
+from duplicity import util
+
+
+test_keys = [
+    ("gpgsm", "\\&165F2FB4F58D537404FE223A603878F54CD444E5", True),
+    ("gpgsm", "\\&86E23738BB09B27C6C7E4F76C39DA0194586CF4B", True),
+    ("gpg", "56538CCF", True),
+    ("gpg", "B5FA894F", False),
+    ("gpg", "9B736B2A", True),
+]
+
+log.setup()
+log.setverbosity(log.DEBUG)
+
+passed = failed = errored = 0
+
+for gpgbin, key, needs_passphrase in test_keys:
+    # set logfile=sys.stdout to see pexpect output
+    res = util.key_needs_passphrase(gpgbin, key, logfile=None)
+    if res is None:
+        log.Debug(f"HARD FAIL: gpg-agent failed for {key}")
+        errored += 1
+    elif res == needs_passphrase:
+        log.Debug(f"PASS: {key} needs passphrase={needs_passphrase} OK")
+        passed += 1
+    else:
+        log.Debug(f"FAIL: {key} needs passphrase={needs_passphrase} got 
{res=}")
+        failed += 1
+
+    log.Debug("\n========================================\n")
+
+log.Debug(f"{passed=}, {failed=}, {errored=}")
+
+sys.exit(failed)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/duplicity-rel.3.0.6.3/testing/manual/roottest.py 
new/duplicity-rel.3.0.7/testing/manual/roottest.py
--- old/duplicity-rel.3.0.6.3/testing/manual/roottest.py        2025-12-05 
17:21:40.000000000 +0100
+++ new/duplicity-rel.3.0.7/testing/manual/roottest.py  2025-12-31 
13:07:54.000000000 +0100
@@ -83,7 +83,7 @@
             
diffdir.write_block_iter(diffdir.DirSig(selection.Select(seq_path).set_iter()), 
sig)
 
             diffdir.write_block_iter(
-                diffdir.DirDelta(selection.Select(new_path).set_iter(), 
sig.open("rb")),
+                
diffdir.DirDelta_WriteSig(selection.Select(new_path).set_iter(), 
sig.open("rb"), None),
                 diff,
             )
 
@@ -105,7 +105,7 @@
         diff = Path("/tmp/testfiles/output/diff.tar")
 
         diffdir.write_block_iter(diffdir.DirSig(self.get_sel(seq_path)), sig)
-        deltablock = diffdir.DirDelta(self.get_sel(new_path), sig.open("rb"))
+        deltablock = diffdir.DirDelta_WriteSig(self.get_sel(new_path), 
sig.open("rb"), None)
         diffdir.write_block_iter(deltablock, diff)
 
         patchdir.Patch(seq_path, diff.open("rb"))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/duplicity-rel.3.0.6.3/testing/unit/test_diffdir.py 
new/duplicity-rel.3.0.7/testing/unit/test_diffdir.py
--- old/duplicity-rel.3.0.6.3/testing/unit/test_diffdir.py      2025-12-05 
17:21:40.000000000 +0100
+++ new/duplicity-rel.3.0.7/testing/unit/test_diffdir.py        2025-12-31 
13:07:54.000000000 +0100
@@ -72,7 +72,7 @@
         select2 = selection.Select(Path(dirname))
         select2.set_iter()
         diffdir.write_block_iter(
-            diffdir.DirDelta(select2, sigtar_fp),
+            diffdir.DirDelta_WriteSig(select2, sigtar_fp, None),
             f"{_runtest_dir}/testfiles/output/difftar",
         )
 
@@ -98,7 +98,7 @@
         select2 = 
selection.Select(Path(f"{_runtest_dir}/testfiles/various_file_types"))
         select2.set_iter()
         diffdir.write_block_iter(
-            diffdir.DirDelta(select2, sigtar_fp),
+            diffdir.DirDelta_WriteSig(select2, sigtar_fp, None),
             f"{_runtest_dir}/testfiles/output/difftar",
         )
 
@@ -119,7 +119,7 @@
 
         sigtar_fp = open(f"{_runtest_dir}/testfiles/output/dir1.sigtar", "rb")
         sel2 = selection.Select(Path(f"{_runtest_dir}/testfiles/dir2"))
-        delta_tar = diffdir.DirDelta(sel2.set_iter(), sigtar_fp)
+        delta_tar = diffdir.DirDelta_WriteSig(sel2.set_iter(), sigtar_fp, None)
         diffdir.write_block_iter(delta_tar, 
f"{_runtest_dir}/testfiles/output/dir1dir2.difftar")
 
         changed_files = [
@@ -147,7 +147,7 @@
 
         sigtar_fp = open(f"{_runtest_dir}/testfiles/output/dir2.sigtar", "rb")
         sel2 = selection.Select(Path(f"{_runtest_dir}/testfiles/dir3"))
-        delta_tar = diffdir.DirDelta(sel2.set_iter(), sigtar_fp)
+        delta_tar = diffdir.DirDelta_WriteSig(sel2.set_iter(), sigtar_fp, None)
         diffdir.write_block_iter(delta_tar, 
f"{_runtest_dir}/testfiles/output/dir2dir3.difftar")
 
         buffer = b""
@@ -199,7 +199,9 @@
             incsig = Path(f"{_runtest_dir}/testfiles/output/incsig." + dirname)
 
             # Write old-style delta to deltadir1
-            diffdir.write_block_iter(diffdir.DirDelta(get_sel(cur_dir), 
old_full_sigs.open("rb")), delta1)
+            diffdir.write_block_iter(
+                diffdir.DirDelta_WriteSig(get_sel(cur_dir), 
old_full_sigs.open("rb"), None), delta1
+            )
 
             # Write new signature and delta to deltadir2 and sigdir2, compare
             block_iter = diffdir.DirDelta_WriteSig(

Reply via email to