Divyansh Singh 2 years ago
parent
commit
32ab17fa46

+ 2 - 0
.drone.yml

@@ -24,6 +24,8 @@ clone:
 steps:
 steps:
 - name: build
 - name: build
   image: alpine:3.13
   image: alpine:3.13
+  environment:
+    TEST_CONTAINER: true
   pull: always
   pull: always
   commands:
   commands:
     - scripts/run-full-tests
     - scripts/run-full-tests

+ 9 - 0
NEWS

@@ -1,3 +1,12 @@
+1.7.11.dev0
+ * #479: Automatically use the "archive_name_format" option to filter which archives get used for
+   borgmatic actions that operate on multiple archives. See the documentation for more information:
+   https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#archive-naming
+ * #479: The "prefix" options have been deprecated in favor of the new "archive_name_format"
+   auto-matching behavior (see above).
+ * #662: Fix regression in which "check_repositories" option failed to match repositories.
+ * #663: Fix regression in which the "transfer" action produced a traceback.
+
 1.7.10
 1.7.10
  * #396: When a database command errors, display and log the error message instead of swallowing it.
  * #396: When a database command errors, display and log the error message instead of swallowing it.
  * #501: Optionally error if a source directory does not exist via "source_directories_must_exist"
  * #501: Optionally error if a source directory does not exist via "source_directories_must_exist"

+ 2 - 2
borgmatic/actions/transfer.py

@@ -17,10 +17,10 @@ def run_transfer(
     '''
     '''
     Run the "transfer" action for the given repository.
     Run the "transfer" action for the given repository.
     '''
     '''
-    logger.info(f'{repository}: Transferring archives to repository')
+    logger.info(f'{repository["path"]}: Transferring archives to repository')
     borgmatic.borg.transfer.transfer_archives(
     borgmatic.borg.transfer.transfer_archives(
         global_arguments.dry_run,
         global_arguments.dry_run,
-        repository,
+        repository['path'],
         storage,
         storage,
         local_borg_version,
         local_borg_version,
         transfer_arguments,
         transfer_arguments,

+ 19 - 10
borgmatic/borg/check.py

@@ -12,7 +12,6 @@ DEFAULT_CHECKS = (
     {'name': 'repository', 'frequency': '1 month'},
     {'name': 'repository', 'frequency': '1 month'},
     {'name': 'archives', 'frequency': '1 month'},
     {'name': 'archives', 'frequency': '1 month'},
 )
 )
-DEFAULT_PREFIX = '{hostname}-'  # noqa: FS003
 
 
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
@@ -146,9 +145,10 @@ def filter_checks_on_frequency(
     return tuple(filtered_checks)
     return tuple(filtered_checks)
 
 
 
 
-def make_check_flags(local_borg_version, checks, check_last=None, prefix=None):
+def make_check_flags(local_borg_version, storage_config, checks, check_last=None, prefix=None):
     '''
     '''
-    Given the local Borg version and a parsed sequence of checks, transform the checks into tuple of
+    Given the local Borg version, a storge configuration dict, a parsed sequence of checks, the
+    check last value, and a consistency check prefix, transform the checks into tuple of
     command-line flags.
     command-line flags.
 
 
     For example, given parsed checks of:
     For example, given parsed checks of:
@@ -174,10 +174,19 @@ def make_check_flags(local_borg_version, checks, check_last=None, prefix=None):
 
 
     if 'archives' in checks:
     if 'archives' in checks:
         last_flags = ('--last', str(check_last)) if check_last else ()
         last_flags = ('--last', str(check_last)) if check_last else ()
-        if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version):
-            match_archives_flags = ('--match-archives', f'sh:{prefix}*') if prefix else ()
-        else:
-            match_archives_flags = ('--glob-archives', f'{prefix}*') if prefix else ()
+        match_archives_flags = (
+            (
+                ('--match-archives', f'sh:{prefix}*')
+                if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version)
+                else ('--glob-archives', f'{prefix}*')
+            )
+            if prefix
+            else (
+                flags.make_match_archives_flags(
+                    storage_config.get('archive_name_format'), local_borg_version
+                )
+            )
+        )
     else:
     else:
         last_flags = ()
         last_flags = ()
         match_archives_flags = ()
         match_archives_flags = ()
@@ -291,7 +300,7 @@ def check_archives(
     extra_borg_options = storage_config.get('extra_borg_options', {}).get('check', '')
     extra_borg_options = storage_config.get('extra_borg_options', {}).get('check', '')
 
 
     if set(checks).intersection({'repository', 'archives', 'data'}):
     if set(checks).intersection({'repository', 'archives', 'data'}):
-        lock_wait = storage_config.get('lock_wait', None)
+        lock_wait = storage_config.get('lock_wait')
 
 
         verbosity_flags = ()
         verbosity_flags = ()
         if logger.isEnabledFor(logging.INFO):
         if logger.isEnabledFor(logging.INFO):
@@ -299,12 +308,12 @@ def check_archives(
         if logger.isEnabledFor(logging.DEBUG):
         if logger.isEnabledFor(logging.DEBUG):
             verbosity_flags = ('--debug', '--show-rc')
             verbosity_flags = ('--debug', '--show-rc')
 
 
-        prefix = consistency_config.get('prefix', DEFAULT_PREFIX)
+        prefix = consistency_config.get('prefix')
 
 
         full_command = (
         full_command = (
             (local_path, 'check')
             (local_path, 'check')
             + (('--repair',) if repair else ())
             + (('--repair',) if repair else ())
-            + make_check_flags(local_borg_version, checks, check_last, prefix)
+            + make_check_flags(local_borg_version, storage_config, checks, check_last, prefix)
             + (('--remote-path', remote_path) if remote_path else ())
             + (('--remote-path', remote_path) if remote_path else ())
             + (('--lock-wait', str(lock_wait)) if lock_wait else ())
             + (('--lock-wait', str(lock_wait)) if lock_wait else ())
             + verbosity_flags
             + verbosity_flags

+ 18 - 0
borgmatic/borg/flags.py

@@ -1,4 +1,5 @@
 import itertools
 import itertools
+import re
 
 
 from borgmatic.borg import feature
 from borgmatic.borg import feature
 
 
@@ -56,3 +57,20 @@ def make_repository_archive_flags(repository_path, archive, local_borg_version):
         if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version)
         if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version)
         else (f'{repository_path}::{archive}',)
         else (f'{repository_path}::{archive}',)
     )
     )
+
+
+def make_match_archives_flags(archive_name_format, local_borg_version):
+    '''
+    Return the match archives flags that would match archives created with the given archive name
+    format (if any). This is done by replacing certain archive name format placeholders for
+    ephemeral data (like "{now}") with globs.
+    '''
+    if not archive_name_format:
+        return ()
+
+    match_archives = re.sub(r'\{(now|utcnow|pid)([:%\w\.-]*)\}', '*', archive_name_format)
+
+    if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version):
+        return ('--match-archives', f'sh:{match_archives}')
+    else:
+        return ('--glob-archives', f'{match_archives}')

+ 5 - 1
borgmatic/borg/info.py

@@ -44,7 +44,11 @@ def display_archives_info(
                 else flags.make_flags('glob-archives', f'{info_arguments.prefix}*')
                 else flags.make_flags('glob-archives', f'{info_arguments.prefix}*')
             )
             )
             if info_arguments.prefix
             if info_arguments.prefix
-            else ()
+            else (
+                flags.make_match_archives_flags(
+                    storage_config.get('archive_name_format'), local_borg_version
+                )
+            )
         )
         )
         + flags.make_flags_from_arguments(
         + flags.make_flags_from_arguments(
             info_arguments, excludes=('repository', 'archive', 'prefix')
             info_arguments, excludes=('repository', 'archive', 'prefix')

+ 12 - 10
borgmatic/borg/prune.py

@@ -7,10 +7,10 @@ from borgmatic.execute import execute_command
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 
 
-def make_prune_flags(retention_config, local_borg_version):
+def make_prune_flags(storage_config, retention_config, local_borg_version):
     '''
     '''
-    Given a retention config dict mapping from option name to value, transform it into an iterable of
-    command-line name-value flag pairs.
+    Given a retention config dict mapping from option name to value, transform it into an sequence of
+    command-line flags.
 
 
     For example, given a retention config of:
     For example, given a retention config of:
 
 
@@ -24,7 +24,7 @@ def make_prune_flags(retention_config, local_borg_version):
         )
         )
     '''
     '''
     config = retention_config.copy()
     config = retention_config.copy()
-    prefix = config.pop('prefix', '{hostname}-')  # noqa: FS003
+    prefix = config.pop('prefix', None)
 
 
     if prefix:
     if prefix:
         if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version):
         if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version):
@@ -32,10 +32,16 @@ def make_prune_flags(retention_config, local_borg_version):
         else:
         else:
             config['glob_archives'] = f'{prefix}*'
             config['glob_archives'] = f'{prefix}*'
 
 
-    return (
+    flag_pairs = (
         ('--' + option_name.replace('_', '-'), str(value)) for option_name, value in config.items()
         ('--' + option_name.replace('_', '-'), str(value)) for option_name, value in config.items()
     )
     )
 
 
+    return tuple(
+        element for pair in flag_pairs for element in pair
+    ) + flags.make_match_archives_flags(
+        storage_config.get('archive_name_format'), local_borg_version
+    )
+
 
 
 def prune_archives(
 def prune_archives(
     dry_run,
     dry_run,
@@ -60,11 +66,7 @@ def prune_archives(
 
 
     full_command = (
     full_command = (
         (local_path, 'prune')
         (local_path, 'prune')
-        + tuple(
-            element
-            for pair in make_prune_flags(retention_config, local_borg_version)
-            for element in pair
-        )
+        + make_prune_flags(storage_config, retention_config, local_borg_version)
         + (('--remote-path', remote_path) if remote_path else ())
         + (('--remote-path', remote_path) if remote_path else ())
         + (('--umask', str(umask)) if umask else ())
         + (('--umask', str(umask)) if umask else ())
         + (('--lock-wait', str(lock_wait)) if lock_wait else ())
         + (('--lock-wait', str(lock_wait)) if lock_wait else ())

+ 5 - 1
borgmatic/borg/rlist.py

@@ -94,7 +94,11 @@ def make_rlist_command(
                 else flags.make_flags('glob-archives', f'{rlist_arguments.prefix}*')
                 else flags.make_flags('glob-archives', f'{rlist_arguments.prefix}*')
             )
             )
             if rlist_arguments.prefix
             if rlist_arguments.prefix
-            else ()
+            else (
+                flags.make_match_archives_flags(
+                    storage_config.get('archive_name_format'), local_borg_version
+                )
+            )
         )
         )
         + flags.make_flags_from_arguments(rlist_arguments, excludes=MAKE_FLAGS_EXCLUDES)
         + flags.make_flags_from_arguments(rlist_arguments, excludes=MAKE_FLAGS_EXCLUDES)
         + flags.make_repository_flags(repository_path, local_borg_version)
         + flags.make_repository_flags(repository_path, local_borg_version)

+ 10 - 3
borgmatic/borg/transfer.py

@@ -34,9 +34,16 @@ def transfer_archives(
                 'match-archives', transfer_arguments.match_archives or transfer_arguments.archive
                 'match-archives', transfer_arguments.match_archives or transfer_arguments.archive
             )
             )
         )
         )
-        + flags.make_flags_from_arguments(
-            transfer_arguments,
-            excludes=('repository', 'source_repository', 'archive', 'match_archives'),
+        + (
+            flags.make_flags_from_arguments(
+                transfer_arguments,
+                excludes=('repository', 'source_repository', 'archive', 'match_archives'),
+            )
+            or (
+                flags.make_match_archives_flags(
+                    storage_config.get('archive_name_format'), local_borg_version
+                )
+            )
         )
         )
         + flags.make_repository_flags(repository_path, local_borg_version)
         + flags.make_repository_flags(repository_path, local_borg_version)
         + flags.make_flags('other-repo', transfer_arguments.source_repository)
         + flags.make_flags('other-repo', transfer_arguments.source_repository)

+ 21 - 20
borgmatic/config/schema.yaml

@@ -378,11 +378,9 @@ properties:
                 description: |
                 description: |
                     Name of the archive. Borg placeholders can be used. See the
                     Name of the archive. Borg placeholders can be used. See the
                     output of "borg help placeholders" for details. Defaults to
                     output of "borg help placeholders" for details. Defaults to
-                    "{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}". If you specify this
-                    option, consider also specifying a prefix in the retention
-                    and consistency sections to avoid accidental
-                    pruning/checking of archives with different archive name
-                    formats.
+                    "{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}". When running
+                    actions like rlist, info, or check, borgmatic automatically
+                    tries to match only archives created with this name format.
                 example: "{hostname}-documents-{now}"
                 example: "{hostname}-documents-{now}"
             relocated_repo_access_is_ok:
             relocated_repo_access_is_ok:
                 type: boolean
                 type: boolean
@@ -477,10 +475,12 @@ properties:
             prefix:
             prefix:
                 type: string
                 type: string
                 description: |
                 description: |
-                    When pruning, only consider archive names starting with this
-                    prefix.  Borg placeholders can be used. See the output of
-                    "borg help placeholders" for details. Defaults to
-                    "{hostname}-". Use an empty value to disable the default.
+                    Deprecated. When pruning, only consider archive names
+                    starting with this prefix. Borg placeholders can be used.
+                    See the output of "borg help placeholders" for details.
+                    If a prefix is not specified, borgmatic defaults to
+                    matching archives based on the archive_name_format (see
+                    above).
                 example: sourcehostname
                 example: sourcehostname
     consistency:
     consistency:
         type: object
         type: object
@@ -538,12 +538,12 @@ properties:
                 items:
                 items:
                     type: string
                     type: string
                 description: |
                 description: |
-                    Paths to a subset of the repositories in the location
-                    section on which to run consistency checks. Handy in case
-                    some of your repositories are very large, and so running
-                    consistency checks on them would take too long. Defaults to
-                    running consistency checks on all repositories configured in
-                    the location section.
+                    Paths or labels for a subset of the repositories in the
+                    location section on which to run consistency checks. Handy
+                    in case some of your repositories are very large, and so
+                    running consistency checks on them would take too long.
+                    Defaults to running consistency checks on all repositories
+                    configured in the location section.
                 example:
                 example:
                     - user@backupserver:sourcehostname.borg
                     - user@backupserver:sourcehostname.borg
             check_last:
             check_last:
@@ -556,11 +556,12 @@ properties:
             prefix:
             prefix:
                 type: string
                 type: string
                 description: |
                 description: |
-                    When performing the "archives" check, only consider archive
-                    names starting with this prefix. Borg placeholders can be
-                    used. See the output of "borg help placeholders" for
-                    details. Defaults to "{hostname}-". Use an empty value to
-                    disable the default.
+                    Deprecated. When performing the "archives" check, only
+                    consider archive names starting with this prefix. Borg
+                    placeholders can be used. See the output of "borg help
+                    placeholders" for details. If a prefix is not specified,
+                    borgmatic defaults to matching archives based on the
+                    archive_name_format (see above).
                 example: sourcehostname
                 example: sourcehostname
     output:
     output:
         type: object
         type: object

+ 4 - 1
borgmatic/config/validate.py

@@ -69,7 +69,10 @@ def apply_logical_validation(config_filename, parsed_configuration):
     location_repositories = parsed_configuration.get('location', {}).get('repositories')
     location_repositories = parsed_configuration.get('location', {}).get('repositories')
     check_repositories = parsed_configuration.get('consistency', {}).get('check_repositories', [])
     check_repositories = parsed_configuration.get('consistency', {}).get('check_repositories', [])
     for repository in check_repositories:
     for repository in check_repositories:
-        if repository not in location_repositories:
+        if not any(
+            repositories_match(repository, config_repository)
+            for config_repository in location_repositories
+        ):
             raise Validation_error(
             raise Validation_error(
                 config_filename,
                 config_filename,
                 (
                 (

+ 1 - 2
docs/how-to/develop-on-borgmatic.md

@@ -25,7 +25,7 @@ so that you can run borgmatic commands while you're hacking on them to
 make sure your changes work.
 make sure your changes work.
 
 
 ```bash
 ```bash
-cd borgmatic/
+cd borgmatic
 pip3 install --user --editable .
 pip3 install --user --editable .
 ```
 ```
 
 
@@ -51,7 +51,6 @@ pip3 install --user tox
 Finally, to actually run tests, run:
 Finally, to actually run tests, run:
 
 
 ```bash
 ```bash
-cd borgmatic
 tox
 tox
 ```
 ```
 
 

+ 71 - 18
docs/how-to/make-per-application-backups.md

@@ -54,6 +54,71 @@ choice](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#autopilot),
 each entry using borgmatic's `--config` flag instead of relying on
 each entry using borgmatic's `--config` flag instead of relying on
 `/etc/borgmatic.d`.
 `/etc/borgmatic.d`.
 
 
+
+## Archive naming
+
+If you've got multiple borgmatic configuration files, you might want to create
+archives with different naming schemes for each one. This is especially handy
+if each configuration file is backing up to the same Borg repository but you
+still want to be able to distinguish backup archives for one application from
+another.
+
+borgmatic supports this use case with an `archive_name_format` option. The
+idea is that you define a string format containing a number of [Borg
+placeholders](https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-placeholders),
+and borgmatic uses that format to name any new archive it creates. For
+instance:
+
+```yaml
+location:
+    ...
+    archive_name_format: home-directories-{now}
+```
+
+This means that when borgmatic creates an archive, its name will start with
+the string `home-directories-` and end with a timestamp for its creation time.
+If `archive_name_format` is unspecified, the default is
+`{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}`, meaning your system hostname plus a
+timestamp in a particular format.
+
+<span class="minilink minilink-addedin">New in version 1.7.11</span> borgmatic
+uses the `archive_name_format` option to automatically limit which archives
+get used for actions operating on multiple archives. This prevents, for
+instance, duplicate archives from showing up in `rlist` or `info` results—even
+if the same repository appears in multiple borgmatic configuration files. To
+take advantage of this feature, simply use a different `archive_name_format`
+in each configuration file.
+
+Under the hood, borgmatic accomplishes this by substituting globs for certain
+ephemeral data placeholders in your `archive_name_format`—and using the result
+to filter archives when running supported actions.
+
+For instance, let's say that you have this in your configuration:
+
+```yaml
+location:
+    ...
+    archive_name_format: {hostname}-user-data-{now}
+```
+
+borgmatic considers `{now}` an emphemeral data placeholder that will probably
+change per archive, while `{hostname}` won't. So it turns the example value
+into `{hostname}-user-data-*` and applies it to filter down the set of
+archives used for actions like `rlist`, `info`, `prune`, `check`, etc.
+
+The end result is that when borgmatic runs the actions for a particular
+application-specific configuration file, it only operates on the archives
+created for that application. Of course, this doesn't apply to actions like
+`compact` that operate on an entire repository.
+
+<span class="minilink minilink-addedin">Prior to 1.7.11</span> The way to
+limit the archives used for the `prune` action was a `prefix` option in the
+`retention` section for matching against the start of archive names. And the
+option for limiting the archives used for the `check` action was a separate
+`prefix` in the `consistency` section. Both of these options are deprecated in
+favor of the auto-matching behavior in newer versions of borgmatic.
+
+
 ## Configuration includes
 ## Configuration includes
 
 
 Once you have multiple different configuration files, you might want to share
 Once you have multiple different configuration files, you might want to share
@@ -272,7 +337,7 @@ Here's an example usage:
 ```yaml
 ```yaml
 constants:
 constants:
     user: foo
     user: foo
-    my_prefix: bar-
+    archive_prefix: bar
 
 
 location:
 location:
     source_directories:
     source_directories:
@@ -281,20 +346,14 @@ location:
     ...
     ...
 
 
 storage:
 storage:
-    archive_name_format: '{my_prefix}{now}'
-
-retention:
-    prefix: {my_prefix}
-
-consistency:
-    prefix: {my_prefix}
+    archive_name_format: '{archive_prefix}-{now}'
 ```
 ```
 
 
 In this example, when borgmatic runs, all instances of `{user}` get replaced
 In this example, when borgmatic runs, all instances of `{user}` get replaced
-with `foo` and all instances of `{my_prefix}` get replaced with `bar-`. (And
-in this particular example, `{now}` doesn't get replaced with anything, but
-gets passed directly to Borg.) After substitution, the logical result looks
-something like this:
+with `foo` and all instances of `{archive-prefix}` get replaced with `bar-`.
+(And in this particular example, `{now}` doesn't get replaced with anything,
+but gets passed directly to Borg.) After substitution, the logical result
+looks something like this:
 
 
 ```yaml
 ```yaml
 location:
 location:
@@ -305,12 +364,6 @@ location:
 
 
 storage:
 storage:
     archive_name_format: 'bar-{now}'
     archive_name_format: 'bar-{now}'
-
-retention:
-    prefix: bar-
-
-consistency:
-    prefix: bar-
 ```
 ```
 
 
 An alternate to constants is passing in your values via [environment
 An alternate to constants is passing in your values via [environment

+ 1 - 1
docs/how-to/set-up-backups.md

@@ -90,7 +90,7 @@ installing borgmatic:
  * [Fedora unofficial](https://copr.fedorainfracloud.org/coprs/heffer/borgmatic/)
  * [Fedora unofficial](https://copr.fedorainfracloud.org/coprs/heffer/borgmatic/)
  * [Arch Linux](https://www.archlinux.org/packages/community/any/borgmatic/)
  * [Arch Linux](https://www.archlinux.org/packages/community/any/borgmatic/)
  * [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=borgmatic)
  * [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=borgmatic)
- * [OpenBSD](http://ports.su/sysutils/borgmatic)
+ * [OpenBSD](https://openports.pl/path/sysutils/borgmatic)
  * [openSUSE](https://software.opensuse.org/package/borgmatic)
  * [openSUSE](https://software.opensuse.org/package/borgmatic)
  * [macOS (via Homebrew)](https://formulae.brew.sh/formula/borgmatic)
  * [macOS (via Homebrew)](https://formulae.brew.sh/formula/borgmatic)
  * [macOS (via MacPorts)](https://ports.macports.org/port/borgmatic/)
  * [macOS (via MacPorts)](https://ports.macports.org/port/borgmatic/)

+ 2 - 2
scripts/run-end-to-end-dev-tests

@@ -1,7 +1,7 @@
 #!/bin/sh
 #!/bin/sh
 
 
-# This script is for running all tests, including end-to-end tests, on a developer machine. It sets
-# up database containers to run tests against, runs the tests, and then tears down the containers.
+# This script is for running end-to-end tests on a developer machine. It sets up database containers
+# to run tests against, runs the tests, and then tears down the containers.
 #
 #
 # Run this script from the root directory of the borgmatic source.
 # Run this script from the root directory of the borgmatic source.
 #
 #

+ 8 - 1
scripts/run-full-tests

@@ -8,7 +8,14 @@
 # For more information, see:
 # For more information, see:
 # https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/
 # https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/
 
 
-set -ex
+set -e
+
+if [ -z "$TEST_CONTAINER" ] ; then
+    echo "This script is designed to work inside a test container and is not intended to"
+    echo "be run manually. If you're trying to run borgmatic's end-to-end tests, execute"
+    echo "scripts/run-end-to-end-dev-tests instead."
+    exit 1
+fi
 
 
 apk add --no-cache python3 py3-pip borgbackup postgresql-client mariadb-client mongodb-tools \
 apk add --no-cache python3 py3-pip borgbackup postgresql-client mariadb-client mongodb-tools \
     py3-ruamel.yaml py3-ruamel.yaml.clib bash sqlite
     py3-ruamel.yaml py3-ruamel.yaml.clib bash sqlite

+ 1 - 1
setup.py

@@ -1,6 +1,6 @@
 from setuptools import find_packages, setup
 from setuptools import find_packages, setup
 
 
-VERSION = '1.7.10'
+VERSION = '1.7.11.dev0'
 
 
 
 
 setup(
 setup(

+ 3 - 0
tests/end-to-end/docker-compose.yaml

@@ -17,6 +17,8 @@ services:
       MONGO_INITDB_ROOT_PASSWORD: test
       MONGO_INITDB_ROOT_PASSWORD: test
   tests:
   tests:
     image: alpine:3.13
     image: alpine:3.13
+    environment:
+      TEST_CONTAINER: true
     volumes:
     volumes:
       - "../..:/app:ro"
       - "../..:/app:ro"
     tmpfs:
     tmpfs:
@@ -28,3 +30,4 @@ services:
     depends_on:
     depends_on:
       - postgresql
       - postgresql
       - mysql
       - mysql
+      - mongodb

+ 1 - 1
tests/integration/test_execute.py

@@ -147,7 +147,7 @@ def test_log_outputs_kills_other_processes_when_one_errors():
         ['sleep', '2'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
         ['sleep', '2'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
     )
     )
     flexmock(module).should_receive('exit_code_indicates_error').with_args(
     flexmock(module).should_receive('exit_code_indicates_error').with_args(
-        other_process, None, 'borg'
+        ['sleep', '2'], None, 'borg'
     ).and_return(False)
     ).and_return(False)
     flexmock(module).should_receive('output_buffer_for_process').with_args(process, ()).and_return(
     flexmock(module).should_receive('output_buffer_for_process').with_args(process, ()).and_return(
         process.stdout
         process.stdout

+ 1 - 1
tests/unit/actions/test_transfer.py

@@ -10,7 +10,7 @@ def test_run_transfer_does_not_raise():
     global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
     global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
 
 
     module.run_transfer(
     module.run_transfer(
-        repository='repo',
+        repository={'path': 'repo'},
         storage={},
         storage={},
         local_borg_version=None,
         local_borg_version=None,
         transfer_arguments=transfer_arguments,
         transfer_arguments=transfer_arguments,

+ 55 - 34
tests/unit/borg/test_check.py

@@ -189,150 +189,170 @@ def test_filter_checks_on_frequency_restains_check_with_unelapsed_frequency_and_
 
 
 def test_make_check_flags_with_repository_check_returns_flag():
 def test_make_check_flags_with_repository_check_returns_flag():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    flags = module.make_check_flags('1.2.3', ('repository',))
+    flags = module.make_check_flags('1.2.3', {}, ('repository',))
 
 
     assert flags == ('--repository-only',)
     assert flags == ('--repository-only',)
 
 
 
 
 def test_make_check_flags_with_archives_check_returns_flag():
 def test_make_check_flags_with_archives_check_returns_flag():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    flags = module.make_check_flags('1.2.3', ('archives',))
+    flags = module.make_check_flags('1.2.3', {}, ('archives',))
 
 
     assert flags == ('--archives-only',)
     assert flags == ('--archives-only',)
 
 
 
 
 def test_make_check_flags_with_data_check_returns_flag_and_implies_archives():
 def test_make_check_flags_with_data_check_returns_flag_and_implies_archives():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    flags = module.make_check_flags('1.2.3', ('data',))
+    flags = module.make_check_flags('1.2.3', {}, ('data',))
 
 
     assert flags == ('--archives-only', '--verify-data',)
     assert flags == ('--archives-only', '--verify-data',)
 
 
 
 
 def test_make_check_flags_with_extract_omits_extract_flag():
 def test_make_check_flags_with_extract_omits_extract_flag():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    flags = module.make_check_flags('1.2.3', ('extract',))
+    flags = module.make_check_flags('1.2.3', {}, ('extract',))
 
 
     assert flags == ()
     assert flags == ()
 
 
 
 
 def test_make_check_flags_with_repository_and_data_checks_does_not_return_repository_only():
 def test_make_check_flags_with_repository_and_data_checks_does_not_return_repository_only():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    flags = module.make_check_flags('1.2.3', ('repository', 'data',))
+    flags = module.make_check_flags('1.2.3', {}, ('repository', 'data',))
 
 
     assert flags == ('--verify-data',)
     assert flags == ('--verify-data',)
 
 
 
 
-def test_make_check_flags_with_default_checks_and_default_prefix_returns_default_flags():
+def test_make_check_flags_with_default_checks_and_prefix_returns_default_flags():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    flags = module.make_check_flags(
-        '1.2.3', ('repository', 'archives'), prefix=module.DEFAULT_PREFIX
-    )
+    flags = module.make_check_flags('1.2.3', {}, ('repository', 'archives'), prefix='foo',)
 
 
-    assert flags == ('--match-archives', f'sh:{module.DEFAULT_PREFIX}*')
+    assert flags == ('--match-archives', 'sh:foo*')
 
 
 
 
-def test_make_check_flags_with_all_checks_and_default_prefix_returns_default_flags():
+def test_make_check_flags_with_all_checks_and_prefix_returns_default_flags():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
     flags = module.make_check_flags(
     flags = module.make_check_flags(
-        '1.2.3', ('repository', 'archives', 'extract'), prefix=module.DEFAULT_PREFIX
+        '1.2.3', {}, ('repository', 'archives', 'extract'), prefix='foo',
     )
     )
 
 
-    assert flags == ('--match-archives', f'sh:{module.DEFAULT_PREFIX}*')
+    assert flags == ('--match-archives', 'sh:foo*')
 
 
 
 
-def test_make_check_flags_with_all_checks_and_default_prefix_without_borg_features_returns_glob_archives_flags():
+def test_make_check_flags_with_all_checks_and_prefix_without_borg_features_returns_glob_archives_flags():
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.feature).should_receive('available').and_return(False)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
     flags = module.make_check_flags(
     flags = module.make_check_flags(
-        '1.2.3', ('repository', 'archives', 'extract'), prefix=module.DEFAULT_PREFIX
+        '1.2.3', {}, ('repository', 'archives', 'extract'), prefix='foo',
     )
     )
 
 
-    assert flags == ('--glob-archives', f'{module.DEFAULT_PREFIX}*')
+    assert flags == ('--glob-archives', 'foo*')
 
 
 
 
 def test_make_check_flags_with_archives_check_and_last_includes_last_flag():
 def test_make_check_flags_with_archives_check_and_last_includes_last_flag():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    flags = module.make_check_flags('1.2.3', ('archives',), check_last=3)
+    flags = module.make_check_flags('1.2.3', {}, ('archives',), check_last=3)
 
 
     assert flags == ('--archives-only', '--last', '3')
     assert flags == ('--archives-only', '--last', '3')
 
 
 
 
 def test_make_check_flags_with_data_check_and_last_includes_last_flag():
 def test_make_check_flags_with_data_check_and_last_includes_last_flag():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    flags = module.make_check_flags('1.2.3', ('data',), check_last=3)
+    flags = module.make_check_flags('1.2.3', {}, ('data',), check_last=3)
 
 
     assert flags == ('--archives-only', '--last', '3', '--verify-data')
     assert flags == ('--archives-only', '--last', '3', '--verify-data')
 
 
 
 
 def test_make_check_flags_with_repository_check_and_last_omits_last_flag():
 def test_make_check_flags_with_repository_check_and_last_omits_last_flag():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    flags = module.make_check_flags('1.2.3', ('repository',), check_last=3)
+    flags = module.make_check_flags('1.2.3', {}, ('repository',), check_last=3)
 
 
     assert flags == ('--repository-only',)
     assert flags == ('--repository-only',)
 
 
 
 
 def test_make_check_flags_with_default_checks_and_last_includes_last_flag():
 def test_make_check_flags_with_default_checks_and_last_includes_last_flag():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    flags = module.make_check_flags('1.2.3', ('repository', 'archives'), check_last=3)
+    flags = module.make_check_flags('1.2.3', {}, ('repository', 'archives'), check_last=3)
 
 
     assert flags == ('--last', '3')
     assert flags == ('--last', '3')
 
 
 
 
 def test_make_check_flags_with_archives_check_and_prefix_includes_match_archives_flag():
 def test_make_check_flags_with_archives_check_and_prefix_includes_match_archives_flag():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    flags = module.make_check_flags('1.2.3', ('archives',), prefix='foo-')
+    flags = module.make_check_flags('1.2.3', {}, ('archives',), prefix='foo-')
 
 
     assert flags == ('--archives-only', '--match-archives', 'sh:foo-*')
     assert flags == ('--archives-only', '--match-archives', 'sh:foo-*')
 
 
 
 
 def test_make_check_flags_with_data_check_and_prefix_includes_match_archives_flag():
 def test_make_check_flags_with_data_check_and_prefix_includes_match_archives_flag():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    flags = module.make_check_flags('1.2.3', ('data',), prefix='foo-')
+    flags = module.make_check_flags('1.2.3', {}, ('data',), prefix='foo-')
 
 
     assert flags == ('--archives-only', '--match-archives', 'sh:foo-*', '--verify-data')
     assert flags == ('--archives-only', '--match-archives', 'sh:foo-*', '--verify-data')
 
 
 
 
-def test_make_check_flags_with_archives_check_and_empty_prefix_omits_match_archives_flag():
+def test_make_check_flags_with_archives_check_and_empty_prefix_uses_archive_name_format_instead():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        'bar-{now}', '1.2.3'  # noqa: FS003
+    ).and_return(('--match-archives', 'sh:bar-*'))
 
 
-    flags = module.make_check_flags('1.2.3', ('archives',), prefix='')
+    flags = module.make_check_flags(
+        '1.2.3', {'archive_name_format': 'bar-{now}'}, ('archives',), prefix=''  # noqa: FS003
+    )
 
 
-    assert flags == ('--archives-only',)
+    assert flags == ('--archives-only', '--match-archives', 'sh:bar-*')
 
 
 
 
 def test_make_check_flags_with_archives_check_and_none_prefix_omits_match_archives_flag():
 def test_make_check_flags_with_archives_check_and_none_prefix_omits_match_archives_flag():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    flags = module.make_check_flags('1.2.3', ('archives',), prefix=None)
+    flags = module.make_check_flags('1.2.3', {}, ('archives',), prefix=None)
 
 
     assert flags == ('--archives-only',)
     assert flags == ('--archives-only',)
 
 
 
 
 def test_make_check_flags_with_repository_check_and_prefix_omits_match_archives_flag():
 def test_make_check_flags_with_repository_check_and_prefix_omits_match_archives_flag():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    flags = module.make_check_flags('1.2.3', ('repository',), prefix='foo-')
+    flags = module.make_check_flags('1.2.3', {}, ('repository',), prefix='foo-')
 
 
     assert flags == ('--repository-only',)
     assert flags == ('--repository-only',)
 
 
 
 
 def test_make_check_flags_with_default_checks_and_prefix_includes_match_archives_flag():
 def test_make_check_flags_with_default_checks_and_prefix_includes_match_archives_flag():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    flags = module.make_check_flags('1.2.3', ('repository', 'archives'), prefix='foo-')
+    flags = module.make_check_flags('1.2.3', {}, ('repository', 'archives'), prefix='foo-')
 
 
     assert flags == ('--match-archives', 'sh:foo-*')
     assert flags == ('--match-archives', 'sh:foo-*')
 
 
@@ -427,7 +447,7 @@ def test_check_archives_calls_borg_with_parameters(checks):
         '{"repository": {"id": "repo"}}'
         '{"repository": {"id": "repo"}}'
     )
     )
     flexmock(module).should_receive('make_check_flags').with_args(
     flexmock(module).should_receive('make_check_flags').with_args(
-        '1.2.3', checks, check_last, module.DEFAULT_PREFIX
+        '1.2.3', {}, checks, check_last, prefix=None,
     ).and_return(())
     ).and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     insert_execute_command_mock(('borg', 'check', 'repo'))
     insert_execute_command_mock(('borg', 'check', 'repo'))
@@ -581,7 +601,7 @@ def test_check_archives_with_local_path_calls_borg_via_local_path():
         '{"repository": {"id": "repo"}}'
         '{"repository": {"id": "repo"}}'
     )
     )
     flexmock(module).should_receive('make_check_flags').with_args(
     flexmock(module).should_receive('make_check_flags').with_args(
-        '1.2.3', checks, check_last, module.DEFAULT_PREFIX
+        '1.2.3', {}, checks, check_last, prefix=None,
     ).and_return(())
     ).and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     insert_execute_command_mock(('borg1', 'check', 'repo'))
     insert_execute_command_mock(('borg1', 'check', 'repo'))
@@ -608,7 +628,7 @@ def test_check_archives_with_remote_path_calls_borg_with_remote_path_parameters(
         '{"repository": {"id": "repo"}}'
         '{"repository": {"id": "repo"}}'
     )
     )
     flexmock(module).should_receive('make_check_flags').with_args(
     flexmock(module).should_receive('make_check_flags').with_args(
-        '1.2.3', checks, check_last, module.DEFAULT_PREFIX
+        '1.2.3', {}, checks, check_last, prefix=None,
     ).and_return(())
     ).and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     insert_execute_command_mock(('borg', 'check', '--remote-path', 'borg1', 'repo'))
     insert_execute_command_mock(('borg', 'check', '--remote-path', 'borg1', 'repo'))
@@ -628,6 +648,7 @@ def test_check_archives_with_remote_path_calls_borg_with_remote_path_parameters(
 def test_check_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
 def test_check_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
     checks = ('repository',)
     checks = ('repository',)
     check_last = flexmock()
     check_last = flexmock()
+    storage_config = {'lock_wait': 5}
     consistency_config = {'check_last': check_last}
     consistency_config = {'check_last': check_last}
     flexmock(module).should_receive('parse_checks')
     flexmock(module).should_receive('parse_checks')
     flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
     flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
@@ -635,7 +656,7 @@ def test_check_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
         '{"repository": {"id": "repo"}}'
         '{"repository": {"id": "repo"}}'
     )
     )
     flexmock(module).should_receive('make_check_flags').with_args(
     flexmock(module).should_receive('make_check_flags').with_args(
-        '1.2.3', checks, check_last, module.DEFAULT_PREFIX
+        '1.2.3', storage_config, checks, check_last, None,
     ).and_return(())
     ).and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     insert_execute_command_mock(('borg', 'check', '--lock-wait', '5', 'repo'))
     insert_execute_command_mock(('borg', 'check', '--lock-wait', '5', 'repo'))
@@ -645,7 +666,7 @@ def test_check_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
     module.check_archives(
     module.check_archives(
         repository_path='repo',
         repository_path='repo',
         location_config={},
         location_config={},
-        storage_config={'lock_wait': 5},
+        storage_config=storage_config,
         consistency_config=consistency_config,
         consistency_config=consistency_config,
         local_borg_version='1.2.3',
         local_borg_version='1.2.3',
     )
     )
@@ -662,7 +683,7 @@ def test_check_archives_with_retention_prefix():
         '{"repository": {"id": "repo"}}'
         '{"repository": {"id": "repo"}}'
     )
     )
     flexmock(module).should_receive('make_check_flags').with_args(
     flexmock(module).should_receive('make_check_flags').with_args(
-        '1.2.3', checks, check_last, prefix
+        '1.2.3', {}, checks, check_last, prefix
     ).and_return(())
     ).and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     insert_execute_command_mock(('borg', 'check', 'repo'))
     insert_execute_command_mock(('borg', 'check', 'repo'))

+ 33 - 0
tests/unit/borg/test_flags.py

@@ -1,3 +1,4 @@
+import pytest
 from flexmock import flexmock
 from flexmock import flexmock
 
 
 from borgmatic.borg import flags as module
 from borgmatic.borg import flags as module
@@ -78,3 +79,35 @@ def test_make_repository_archive_flags_with_borg_features_joins_repository_and_a
     assert module.make_repository_archive_flags(
     assert module.make_repository_archive_flags(
         repository_path='repo', archive='archive', local_borg_version='1.2.3'
         repository_path='repo', archive='archive', local_borg_version='1.2.3'
     ) == ('repo::archive',)
     ) == ('repo::archive',)
+
+
+@pytest.mark.parametrize(
+    'archive_name_format,feature_available,expected_result',
+    (
+        (None, True, ()),
+        ('', True, ()),
+        (
+            '{hostname}-docs-{now}',  # noqa: FS003
+            True,
+            ('--match-archives', 'sh:{hostname}-docs-*'),  # noqa: FS003
+        ),
+        ('{utcnow}-docs-{user}', True, ('--match-archives', 'sh:*-docs-{user}')),  # noqa: FS003
+        ('{fqdn}-{pid}', True, ('--match-archives', 'sh:{fqdn}-*')),  # noqa: FS003
+        (
+            'stuff-{now:%Y-%m-%dT%H:%M:%S.%f}',  # noqa: FS003
+            True,
+            ('--match-archives', 'sh:stuff-*'),
+        ),
+        ('{hostname}-docs-{now}', False, ('--glob-archives', '{hostname}-docs-*')),  # noqa: FS003
+        ('{utcnow}-docs-{user}', False, ('--glob-archives', '*-docs-{user}')),  # noqa: FS003
+    ),
+)
+def test_make_match_archives_flags_makes_flags_with_globs(
+    archive_name_format, feature_available, expected_result
+):
+    flexmock(module.feature).should_receive('available').and_return(feature_available)
+
+    assert (
+        module.make_match_archives_flags(archive_name_format, local_borg_version=flexmock())
+        == expected_result
+    )

+ 90 - 1
tests/unit/borg/test_info.py

@@ -12,6 +12,9 @@ def test_display_archives_info_calls_borg_with_parameters():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '2.3.4'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
@@ -34,6 +37,9 @@ def test_display_archives_info_with_log_info_calls_borg_with_info_parameter():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '2.3.4'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
@@ -56,6 +62,9 @@ def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_outpu
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '2.3.4'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
@@ -78,6 +87,9 @@ def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '2.3.4'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
@@ -101,6 +113,9 @@ def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_outp
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '2.3.4'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
@@ -123,6 +138,9 @@ def test_display_archives_info_with_json_calls_borg_with_json_parameter():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '2.3.4'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
@@ -147,6 +165,9 @@ def test_display_archives_info_with_archive_calls_borg_with_match_archives_param
     flexmock(module.flags).should_receive('make_flags').with_args(
     flexmock(module.flags).should_receive('make_flags').with_args(
         'match-archives', 'archive'
         'match-archives', 'archive'
     ).and_return(('--match-archives', 'archive'))
     ).and_return(('--match-archives', 'archive'))
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '2.3.4'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
@@ -169,6 +190,9 @@ def test_display_archives_info_with_local_path_calls_borg_via_local_path():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '2.3.4'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
@@ -195,6 +219,9 @@ def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_para
     flexmock(module.flags).should_receive('make_flags').with_args(
     flexmock(module.flags).should_receive('make_flags').with_args(
         'remote-path', 'borg1'
         'remote-path', 'borg1'
     ).and_return(('--remote-path', 'borg1'))
     ).and_return(('--remote-path', 'borg1'))
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '2.3.4'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
@@ -221,6 +248,9 @@ def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_paramete
     flexmock(module.flags).should_receive('make_flags').with_args('lock-wait', 5).and_return(
     flexmock(module.flags).should_receive('make_flags').with_args('lock-wait', 5).and_return(
         ('--lock-wait', '5')
         ('--lock-wait', '5')
     )
     )
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '2.3.4'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     storage_config = {'lock_wait': 5}
     storage_config = {'lock_wait': 5}
@@ -240,13 +270,16 @@ def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_paramete
     )
     )
 
 
 
 
-def test_display_archives_info_with_prefix_calls_borg_with_match_archives_parameters():
+def test_display_archives_info_transforms_prefix_into_match_archives_parameters():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args(
     flexmock(module.flags).should_receive('make_flags').with_args(
         'match-archives', 'sh:foo*'
         'match-archives', 'sh:foo*'
     ).and_return(('--match-archives', 'sh:foo*'))
     ).and_return(('--match-archives', 'sh:foo*'))
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '2.3.4'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
@@ -265,12 +298,68 @@ def test_display_archives_info_with_prefix_calls_borg_with_match_archives_parame
     )
     )
 
 
 
 
+def test_display_archives_info_prefers_prefix_over_archive_name_format():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_flags').with_args(
+        'match-archives', 'sh:foo*'
+    ).and_return(('--match-archives', 'sh:foo*'))
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '2.3.4'
+    ).and_return(())
+    flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'info', '--match-archives', 'sh:foo*', '--repo', 'repo'),
+        output_log_level=module.borgmatic.logger.ANSWER,
+        borg_local_path='borg',
+        extra_environment=None,
+    )
+
+    module.display_archives_info(
+        repository_path='repo',
+        storage_config={'archive_name_format': 'bar-{now}'},  # noqa: FS003
+        local_borg_version='2.3.4',
+        info_arguments=flexmock(archive=None, json=False, prefix='foo'),
+    )
+
+
+def test_display_archives_info_transforms_archive_name_format_into_match_archives_parameters():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        'bar-{now}', '2.3.4'  # noqa: FS003
+    ).and_return(('--match-archives', 'sh:bar-*'))
+    flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'info', '--match-archives', 'sh:bar-*', '--repo', 'repo'),
+        output_log_level=module.borgmatic.logger.ANSWER,
+        borg_local_path='borg',
+        extra_environment=None,
+    )
+
+    module.display_archives_info(
+        repository_path='repo',
+        storage_config={'archive_name_format': 'bar-{now}'},  # noqa: FS003
+        local_borg_version='2.3.4',
+        info_arguments=flexmock(archive=None, json=False, prefix=None),
+    )
+
+
 @pytest.mark.parametrize('argument_name', ('match_archives', 'sort_by', 'first', 'last'))
 @pytest.mark.parametrize('argument_name', ('match_archives', 'sort_by', 'first', 'last'))
 def test_display_archives_info_passes_through_arguments_to_borg(argument_name):
 def test_display_archives_info_passes_through_arguments_to_borg(argument_name):
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flag_name = f"--{argument_name.replace('_', ' ')}"
     flag_name = f"--{argument_name.replace('_', ' ')}"
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '2.3.4'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(
         (flag_name, 'value')
         (flag_name, 'value')
     )
     )

+ 32 - 29
tests/unit/borg/test_prune.py

@@ -18,18 +18,17 @@ def insert_execute_command_mock(prune_command, output_log_level):
     ).once()
     ).once()
 
 
 
 
-BASE_PRUNE_FLAGS = (('--keep-daily', '1'), ('--keep-weekly', '2'), ('--keep-monthly', '3'))
+BASE_PRUNE_FLAGS = ('--keep-daily', '1', '--keep-weekly', '2', '--keep-monthly', '3')
 
 
 
 
-def test_make_prune_flags_returns_flags_from_config_plus_default_prefix_glob():
+def test_make_prune_flags_returns_flags_from_config():
     retention_config = OrderedDict((('keep_daily', 1), ('keep_weekly', 2), ('keep_monthly', 3)))
     retention_config = OrderedDict((('keep_daily', 1), ('keep_weekly', 2), ('keep_monthly', 3)))
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    result = module.make_prune_flags(retention_config, local_borg_version='1.2.3')
+    result = module.make_prune_flags({}, retention_config, local_borg_version='1.2.3')
 
 
-    assert tuple(result) == BASE_PRUNE_FLAGS + (
-        ('--match-archives', 'sh:{hostname}-*'),  # noqa: FS003
-    )
+    assert result == BASE_PRUNE_FLAGS
 
 
 
 
 def test_make_prune_flags_accepts_prefix_with_placeholders():
 def test_make_prune_flags_accepts_prefix_with_placeholders():
@@ -37,15 +36,18 @@ def test_make_prune_flags_accepts_prefix_with_placeholders():
         (('keep_daily', 1), ('prefix', 'Documents_{hostname}-{now}'))  # noqa: FS003
         (('keep_daily', 1), ('prefix', 'Documents_{hostname}-{now}'))  # noqa: FS003
     )
     )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    result = module.make_prune_flags(retention_config, local_borg_version='1.2.3')
+    result = module.make_prune_flags({}, retention_config, local_borg_version='1.2.3')
 
 
     expected = (
     expected = (
-        ('--keep-daily', '1'),
-        ('--match-archives', 'sh:Documents_{hostname}-{now}*'),  # noqa: FS003
+        '--keep-daily',
+        '1',
+        '--match-archives',
+        'sh:Documents_{hostname}-{now}*',  # noqa: FS003
     )
     )
 
 
-    assert tuple(result) == expected
+    assert result == expected
 
 
 
 
 def test_make_prune_flags_with_prefix_without_borg_features_uses_glob_archives():
 def test_make_prune_flags_with_prefix_without_borg_features_uses_glob_archives():
@@ -53,37 +55,38 @@ def test_make_prune_flags_with_prefix_without_borg_features_uses_glob_archives()
         (('keep_daily', 1), ('prefix', 'Documents_{hostname}-{now}'))  # noqa: FS003
         (('keep_daily', 1), ('prefix', 'Documents_{hostname}-{now}'))  # noqa: FS003
     )
     )
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.feature).should_receive('available').and_return(False)
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    result = module.make_prune_flags(retention_config, local_borg_version='1.2.3')
+    result = module.make_prune_flags({}, retention_config, local_borg_version='1.2.3')
 
 
     expected = (
     expected = (
-        ('--keep-daily', '1'),
-        ('--glob-archives', 'Documents_{hostname}-{now}*'),  # noqa: FS003
+        '--keep-daily',
+        '1',
+        '--glob-archives',
+        'Documents_{hostname}-{now}*',  # noqa: FS003
     )
     )
 
 
-    assert tuple(result) == expected
-
-
-def test_make_prune_flags_treats_empty_prefix_as_no_prefix():
-    retention_config = OrderedDict((('keep_daily', 1), ('prefix', '')))
-    flexmock(module.feature).should_receive('available').and_return(True)
-
-    result = module.make_prune_flags(retention_config, local_borg_version='1.2.3')
+    assert result == expected
 
 
-    expected = (('--keep-daily', '1'),)
 
 
-    assert tuple(result) == expected
-
-
-def test_make_prune_flags_treats_none_prefix_as_no_prefix():
+def test_make_prune_flags_without_prefix_uses_archive_name_format_instead():
+    storage_config = {'archive_name_format': 'bar-{now}'}  # noqa: FS003
     retention_config = OrderedDict((('keep_daily', 1), ('prefix', None)))
     retention_config = OrderedDict((('keep_daily', 1), ('prefix', None)))
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        'bar-{now}', '1.2.3'  # noqa: FS003
+    ).and_return(('--match-archives', 'sh:bar-*'))
 
 
-    result = module.make_prune_flags(retention_config, local_borg_version='1.2.3')
+    result = module.make_prune_flags(storage_config, retention_config, local_borg_version='1.2.3')
 
 
-    expected = (('--keep-daily', '1'),)
+    expected = (
+        '--keep-daily',
+        '1',
+        '--match-archives',
+        'sh:bar-*',  # noqa: FS003
+    )
 
 
-    assert tuple(result) == expected
+    assert result == expected
 
 
 
 
 PRUNE_COMMAND = ('borg', 'prune', '--keep-daily', '1', '--keep-weekly', '2', '--keep-monthly', '3')
 PRUNE_COMMAND = ('borg', 'prune', '--keep-daily', '1', '--keep-weekly', '2', '--keep-monthly', '3')

+ 69 - 0
tests/unit/borg/test_rlist.py

@@ -127,6 +127,9 @@ def test_resolve_archive_name_with_lock_wait_calls_borg_with_lock_wait_parameter
 def test_make_rlist_command_includes_log_info():
 def test_make_rlist_command_includes_log_info():
     insert_logging_mock(logging.INFO)
     insert_logging_mock(logging.INFO)
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '1.2.3'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
 
 
@@ -143,6 +146,9 @@ def test_make_rlist_command_includes_log_info():
 def test_make_rlist_command_includes_json_but_not_info():
 def test_make_rlist_command_includes_json_but_not_info():
     insert_logging_mock(logging.INFO)
     insert_logging_mock(logging.INFO)
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '1.2.3'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
 
 
@@ -159,6 +165,9 @@ def test_make_rlist_command_includes_json_but_not_info():
 def test_make_rlist_command_includes_log_debug():
 def test_make_rlist_command_includes_log_debug():
     insert_logging_mock(logging.DEBUG)
     insert_logging_mock(logging.DEBUG)
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '1.2.3'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
 
 
@@ -175,6 +184,9 @@ def test_make_rlist_command_includes_log_debug():
 def test_make_rlist_command_includes_json_but_not_debug():
 def test_make_rlist_command_includes_json_but_not_debug():
     insert_logging_mock(logging.DEBUG)
     insert_logging_mock(logging.DEBUG)
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '1.2.3'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
 
 
@@ -190,6 +202,9 @@ def test_make_rlist_command_includes_json_but_not_debug():
 
 
 def test_make_rlist_command_includes_json():
 def test_make_rlist_command_includes_json():
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '1.2.3'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
 
 
@@ -207,6 +222,9 @@ def test_make_rlist_command_includes_lock_wait():
     flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(
     flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(
         ('--lock-wait', '5')
         ('--lock-wait', '5')
     ).and_return(())
     ).and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '1.2.3'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
 
 
@@ -222,6 +240,9 @@ def test_make_rlist_command_includes_lock_wait():
 
 
 def test_make_rlist_command_includes_local_path():
 def test_make_rlist_command_includes_local_path():
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '1.2.3'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
 
 
@@ -240,6 +261,9 @@ def test_make_rlist_command_includes_remote_path():
     flexmock(module.flags).should_receive('make_flags').and_return(
     flexmock(module.flags).should_receive('make_flags').and_return(
         ('--remote-path', 'borg2')
         ('--remote-path', 'borg2')
     ).and_return(()).and_return(())
     ).and_return(()).and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '1.2.3'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
 
 
@@ -258,6 +282,9 @@ def test_make_rlist_command_transforms_prefix_into_match_archives():
     flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
     flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
         ('--match-archives', 'sh:foo*')
         ('--match-archives', 'sh:foo*')
     )
     )
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '1.2.3'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
 
 
@@ -271,8 +298,47 @@ def test_make_rlist_command_transforms_prefix_into_match_archives():
     assert command == ('borg', 'list', '--match-archives', 'sh:foo*', 'repo')
     assert command == ('borg', 'list', '--match-archives', 'sh:foo*', 'repo')
 
 
 
 
+def test_make_rlist_command_prefers_prefix_over_archive_name_format():
+    flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
+        ('--match-archives', 'sh:foo*')
+    )
+    flexmock(module.flags).should_receive('make_match_archives_flags').never()
+    flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+
+    command = module.make_rlist_command(
+        repository_path='repo',
+        storage_config={'archive_name_format': 'bar-{now}'},  # noqa: FS003
+        local_borg_version='1.2.3',
+        rlist_arguments=flexmock(archive=None, paths=None, json=False, prefix='foo'),
+    )
+
+    assert command == ('borg', 'list', '--match-archives', 'sh:foo*', 'repo')
+
+
+def test_make_rlist_command_transforms_archive_name_format_into_match_archives():
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        'bar-{now}', '1.2.3'  # noqa: FS003
+    ).and_return(('--match-archives', 'sh:bar-*'))
+    flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+
+    command = module.make_rlist_command(
+        repository_path='repo',
+        storage_config={'archive_name_format': 'bar-{now}'},  # noqa: FS003
+        local_borg_version='1.2.3',
+        rlist_arguments=flexmock(archive=None, paths=None, json=False, prefix=None),
+    )
+
+    assert command == ('borg', 'list', '--match-archives', 'sh:bar-*', 'repo')
+
+
 def test_make_rlist_command_includes_short():
 def test_make_rlist_command_includes_short():
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '1.2.3'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--short',))
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--short',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
 
 
@@ -301,6 +367,9 @@ def test_make_rlist_command_includes_short():
 )
 )
 def test_make_rlist_command_includes_additional_flags(argument_name):
 def test_make_rlist_command_includes_additional_flags(argument_name):
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None, '1.2.3'
+    ).and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(
         (f"--{argument_name.replace('_', '-')}", 'value')
         (f"--{argument_name.replace('_', '-')}", 'value')
     )
     )

+ 43 - 2
tests/unit/borg/test_transfer.py

@@ -12,6 +12,7 @@ def test_transfer_archives_calls_borg_with_flags():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
@@ -41,6 +42,7 @@ def test_transfer_archives_with_dry_run_calls_borg_with_dry_run_flag():
     flexmock(module.flags).should_receive('make_flags').with_args('dry-run', True).and_return(
     flexmock(module.flags).should_receive('make_flags').with_args('dry-run', True).and_return(
         ('--dry-run',)
         ('--dry-run',)
     )
     )
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
@@ -67,6 +69,7 @@ def test_transfer_archives_with_log_info_calls_borg_with_info_flag():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
@@ -93,6 +96,7 @@ def test_transfer_archives_with_log_debug_calls_borg_with_debug_flag():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
@@ -123,6 +127,7 @@ def test_transfer_archives_with_archive_calls_borg_with_match_archives_flag():
     flexmock(module.flags).should_receive('make_flags').with_args(
     flexmock(module.flags).should_receive('make_flags').with_args(
         'match-archives', 'archive'
         'match-archives', 'archive'
     ).and_return(('--match-archives', 'archive'))
     ).and_return(('--match-archives', 'archive'))
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
@@ -137,7 +142,7 @@ def test_transfer_archives_with_archive_calls_borg_with_match_archives_flag():
     module.transfer_archives(
     module.transfer_archives(
         dry_run=False,
         dry_run=False,
         repository_path='repo',
         repository_path='repo',
-        storage_config={},
+        storage_config={'archive_name_format': 'bar-{now}'},  # noqa: FS003
         local_borg_version='2.3.4',
         local_borg_version='2.3.4',
         transfer_arguments=flexmock(
         transfer_arguments=flexmock(
             archive='archive', progress=None, match_archives=None, source_repository=None
             archive='archive', progress=None, match_archives=None, source_repository=None
@@ -152,6 +157,7 @@ def test_transfer_archives_with_match_archives_calls_borg_with_match_archives_fl
     flexmock(module.flags).should_receive('make_flags').with_args(
     flexmock(module.flags).should_receive('make_flags').with_args(
         'match-archives', 'sh:foo*'
         'match-archives', 'sh:foo*'
     ).and_return(('--match-archives', 'sh:foo*'))
     ).and_return(('--match-archives', 'sh:foo*'))
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
@@ -166,7 +172,7 @@ def test_transfer_archives_with_match_archives_calls_borg_with_match_archives_fl
     module.transfer_archives(
     module.transfer_archives(
         dry_run=False,
         dry_run=False,
         repository_path='repo',
         repository_path='repo',
-        storage_config={},
+        storage_config={'archive_name_format': 'bar-{now}'},  # noqa: FS003
         local_borg_version='2.3.4',
         local_borg_version='2.3.4',
         transfer_arguments=flexmock(
         transfer_arguments=flexmock(
             archive=None, progress=None, match_archives='sh:foo*', source_repository=None
             archive=None, progress=None, match_archives='sh:foo*', source_repository=None
@@ -174,10 +180,40 @@ def test_transfer_archives_with_match_archives_calls_borg_with_match_archives_fl
     )
     )
 
 
 
 
+def test_transfer_archives_with_archive_name_format_calls_borg_with_match_archives_flag():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        'bar-{now}', '2.3.4'  # noqa: FS003
+    ).and_return(('--match-archives', 'sh:bar-*'))
+    flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'transfer', '--match-archives', 'sh:bar-*', '--repo', 'repo'),
+        output_log_level=module.borgmatic.logger.ANSWER,
+        output_file=None,
+        borg_local_path='borg',
+        extra_environment=None,
+    )
+
+    module.transfer_archives(
+        dry_run=False,
+        repository_path='repo',
+        storage_config={'archive_name_format': 'bar-{now}'},  # noqa: FS003
+        local_borg_version='2.3.4',
+        transfer_arguments=flexmock(
+            archive=None, progress=None, match_archives=None, source_repository=None
+        ),
+    )
+
+
 def test_transfer_archives_with_local_path_calls_borg_via_local_path():
 def test_transfer_archives_with_local_path_calls_borg_via_local_path():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
@@ -208,6 +244,7 @@ def test_transfer_archives_with_remote_path_calls_borg_with_remote_path_flags():
     flexmock(module.flags).should_receive('make_flags').with_args(
     flexmock(module.flags).should_receive('make_flags').with_args(
         'remote-path', 'borg2'
         'remote-path', 'borg2'
     ).and_return(('--remote-path', 'borg2'))
     ).and_return(('--remote-path', 'borg2'))
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
@@ -238,6 +275,7 @@ def test_transfer_archives_with_lock_wait_calls_borg_with_lock_wait_flags():
     flexmock(module.flags).should_receive('make_flags').with_args('lock-wait', 5).and_return(
     flexmock(module.flags).should_receive('make_flags').with_args('lock-wait', 5).and_return(
         ('--lock-wait', '5')
         ('--lock-wait', '5')
     )
     )
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     storage_config = {'lock_wait': 5}
     storage_config = {'lock_wait': 5}
@@ -265,6 +303,7 @@ def test_transfer_archives_with_progress_calls_borg_with_progress_flag():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
@@ -293,6 +332,7 @@ def test_transfer_archives_passes_through_arguments_to_borg(argument_name):
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flag_name = f"--{argument_name.replace('_', ' ')}"
     flag_name = f"--{argument_name.replace('_', ' ')}"
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(
         (flag_name, 'value')
         (flag_name, 'value')
     )
     )
@@ -327,6 +367,7 @@ def test_transfer_archives_with_source_repository_calls_borg_with_other_repo_fla
     flexmock(module.flags).should_receive('make_flags').with_args('other-repo', 'other').and_return(
     flexmock(module.flags).should_receive('make_flags').with_args('other-repo', 'other').and_return(
         ('--other-repo', 'other')
         ('--other-repo', 'other')
     )
     )
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')

+ 19 - 3
tests/unit/config/test_validate.py

@@ -37,7 +37,7 @@ def test_validation_error_string_contains_errors():
     assert 'uh oh' in result
     assert 'uh oh' in result
 
 
 
 
-def test_apply_locical_validation_raises_if_unknown_repository_in_check_repositories():
+def test_apply_logical_validation_raises_if_unknown_repository_in_check_repositories():
     flexmock(module).format_json_error = lambda error: error.message
     flexmock(module).format_json_error = lambda error: error.message
 
 
     with pytest.raises(module.Validation_error):
     with pytest.raises(module.Validation_error):
@@ -51,17 +51,33 @@ def test_apply_locical_validation_raises_if_unknown_repository_in_check_reposito
         )
         )
 
 
 
 
-def test_apply_locical_validation_does_not_raise_if_known_repository_in_check_repositories():
+def test_apply_logical_validation_does_not_raise_if_known_repository_path_in_check_repositories():
     module.apply_logical_validation(
     module.apply_logical_validation(
         'config.yaml',
         'config.yaml',
         {
         {
-            'location': {'repositories': ['repo.borg', 'other.borg']},
+            'location': {'repositories': [{'path': 'repo.borg'}, {'path': 'other.borg'}]},
             'retention': {'keep_secondly': 1000},
             'retention': {'keep_secondly': 1000},
             'consistency': {'check_repositories': ['repo.borg']},
             'consistency': {'check_repositories': ['repo.borg']},
         },
         },
     )
     )
 
 
 
 
+def test_apply_logical_validation_does_not_raise_if_known_repository_label_in_check_repositories():
+    module.apply_logical_validation(
+        'config.yaml',
+        {
+            'location': {
+                'repositories': [
+                    {'path': 'repo.borg', 'label': 'my_repo'},
+                    {'path': 'other.borg', 'label': 'other_repo'},
+                ]
+            },
+            'retention': {'keep_secondly': 1000},
+            'consistency': {'check_repositories': ['my_repo']},
+        },
+    )
+
+
 def test_apply_logical_validation_does_not_raise_if_archive_name_format_and_prefix_present():
 def test_apply_logical_validation_does_not_raise_if_archive_name_format_and_prefix_present():
     module.apply_logical_validation(
     module.apply_logical_validation(
         'config.yaml',
         'config.yaml',