Bladeren bron

Borg 2 changes: Default the "archive_name_format" option to just "{hostname}". Update the "--match-archives"/"--archive" flags to support series names / archive hashes. Add a "--match-archives" flag to the "prune" action.

Dan Helfman 7 maanden geleden
bovenliggende
commit
83bc737185

+ 9 - 0
NEWS

@@ -21,6 +21,15 @@
    all repositories in the configuration file.
    all repositories in the configuration file.
  * Add support for Borg 2's "rclone://" repository URLs, so you can backup to 70+ cloud storage
  * Add support for Borg 2's "rclone://" repository URLs, so you can backup to 70+ cloud storage
    services whether or not they support Borg explicitly.
    services whether or not they support Borg explicitly.
+ * When using Borg 2, default the "archive_name_format" option to just "{hostname}", as Borg 2 does
+   not require unique archive names; identical archive names form a common "series" that can be
+   targeted together. See the Borg 2 documentation for more information:
+   https://borgbackup.readthedocs.io/en/2.0.0b12/changes.html#borg-1-2-x-1-4-x-to-borg-2-0
+ * Update the "--match-archives" flag in all actions (and the "--archive" flag in select actions) to
+   support a Borg 2 series name as its value.
+ * Update the "--match-archives" and "--archive" flags in all actions to support a Borg 2 archive
+   hash as its value.
+ * Add a "--match-archives" flag to the "prune" action.
 
 
 1.8.14
 1.8.14
  * #896: Fix an error in borgmatic rcreate/init on an empty repository directory with Borg 1.4.
  * #896: Fix an error in borgmatic rcreate/init on an empty repository directory with Borg 1.4.

+ 3 - 1
borgmatic/borg/create.py

@@ -399,7 +399,9 @@ def make_base_create_command(
     lock_wait = config.get('lock_wait', None)
     lock_wait = config.get('lock_wait', None)
     list_filter_flags = make_list_filter_flags(local_borg_version, dry_run)
     list_filter_flags = make_list_filter_flags(local_borg_version, dry_run)
     files_cache = config.get('files_cache')
     files_cache = config.get('files_cache')
-    archive_name_format = config.get('archive_name_format', flags.DEFAULT_ARCHIVE_NAME_FORMAT)
+    archive_name_format = config.get(
+        'archive_name_format', flags.get_default_archive_name_format(local_borg_version)
+    )
     extra_borg_options = config.get('extra_borg_options', {}).get('create', '')
     extra_borg_options = config.get('extra_borg_options', {}).get('create', '')
 
 
     if feature.available(feature.Feature.ATIME, local_borg_version):
     if feature.available(feature.Feature.ATIME, local_borg_version):

+ 2 - 0
borgmatic/borg/feature.py

@@ -16,6 +16,7 @@ class Feature(Enum):
     REPO_DELETE = 10
     REPO_DELETE = 10
     MATCH_ARCHIVES = 11
     MATCH_ARCHIVES = 11
     EXCLUDED_FILES_MINUS = 12
     EXCLUDED_FILES_MINUS = 12
+    ARCHIVE_SERIES = 13
 
 
 
 
 FEATURE_TO_MINIMUM_BORG_VERSION = {
 FEATURE_TO_MINIMUM_BORG_VERSION = {
@@ -31,6 +32,7 @@ FEATURE_TO_MINIMUM_BORG_VERSION = {
     Feature.REPO_DELETE: parse('2.0.0a2'),  # borg repo-delete
     Feature.REPO_DELETE: parse('2.0.0a2'),  # borg repo-delete
     Feature.MATCH_ARCHIVES: parse('2.0.0b3'),  # borg --match-archives
     Feature.MATCH_ARCHIVES: parse('2.0.0b3'),  # borg --match-archives
     Feature.EXCLUDED_FILES_MINUS: parse('2.0.0b5'),  # --list --filter uses "-" for excludes
     Feature.EXCLUDED_FILES_MINUS: parse('2.0.0b5'),  # --list --filter uses "-" for excludes
+    Feature.ARCHIVE_SERIES: parse('2.0.0b11'),  # identically named archives form a series
 }
 }
 
 
 
 

+ 39 - 4
borgmatic/borg/flags.py

@@ -50,6 +50,9 @@ def make_repository_flags(repository_path, local_borg_version):
     ) + (repository_path,)
     ) + (repository_path,)
 
 
 
 
+ARCHIVE_HASH_PATTERN = re.compile('[0-9a-fA-F]{8,}$')
+
+
 def make_repository_archive_flags(repository_path, archive, local_borg_version):
 def make_repository_archive_flags(repository_path, archive, local_borg_version):
     '''
     '''
     Given the path of a Borg repository, an archive name or pattern, and the local Borg version,
     Given the path of a Borg repository, an archive name or pattern, and the local Borg version,
@@ -57,20 +60,41 @@ def make_repository_archive_flags(repository_path, archive, local_borg_version):
     and archive.
     and archive.
     '''
     '''
     return (
     return (
-        ('--repo', repository_path, archive)
+        (
+            '--repo',
+            repository_path,
+            (
+                f'aid:{archive}'
+                if feature.available(feature.Feature.ARCHIVE_SERIES, local_borg_version)
+                and ARCHIVE_HASH_PATTERN.match(archive)
+                and not archive.startswith('aid:')
+                else archive
+            ),
+        )
         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}',)
     )
     )
 
 
 
 
-DEFAULT_ARCHIVE_NAME_FORMAT = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'  # noqa: FS003
+DEFAULT_ARCHIVE_NAME_FORMAT_WITHOUT_SERIES = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'  # noqa: FS003
+DEFAULT_ARCHIVE_NAME_FORMAT_WITH_SERIES = '{hostname}'  # noqa: FS003
+
+
+def get_default_archive_name_format(local_borg_version):
+    '''
+    Given the local Borg version, return the corresponding default archive name format.
+    '''
+    if feature.available(feature.Feature.ARCHIVE_SERIES, local_borg_version):
+        return DEFAULT_ARCHIVE_NAME_FORMAT_WITH_SERIES
+
+    return DEFAULT_ARCHIVE_NAME_FORMAT_WITHOUT_SERIES
 
 
 
 
 def make_match_archives_flags(
 def make_match_archives_flags(
     match_archives,
     match_archives,
     archive_name_format,
     archive_name_format,
     local_borg_version,
     local_borg_version,
-    default_archive_name_format=DEFAULT_ARCHIVE_NAME_FORMAT,
+    default_archive_name_format=None,
 ):
 ):
     '''
     '''
     Return match archives flags based on the given match archives value, if any. If it isn't set,
     Return match archives flags based on the given match archives value, if any. If it isn't set,
@@ -83,12 +107,23 @@ def make_match_archives_flags(
             return ()
             return ()
 
 
         if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version):
         if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version):
+            if (
+                feature.available(feature.Feature.ARCHIVE_SERIES, local_borg_version)
+                and ARCHIVE_HASH_PATTERN.match(match_archives)
+                and not match_archives.startswith('aid:')
+            ):
+                return ('--match-archives', f'aid:{match_archives}')
+
             return ('--match-archives', match_archives)
             return ('--match-archives', match_archives)
         else:
         else:
             return ('--glob-archives', re.sub(r'^sh:', '', match_archives))
             return ('--glob-archives', re.sub(r'^sh:', '', match_archives))
 
 
     derived_match_archives = re.sub(
     derived_match_archives = re.sub(
-        r'\{(now|utcnow|pid)([:%\w\.-]*)\}', '*', archive_name_format or default_archive_name_format
+        r'\{(now|utcnow|pid)([:%\w\.-]*)\}',
+        '*',
+        archive_name_format
+        or default_archive_name_format
+        or get_default_archive_name_format(local_borg_version),
     )
     )
 
 
     if derived_match_archives == '*':
     if derived_match_archives == '*':

+ 6 - 5
borgmatic/borg/prune.py

@@ -8,9 +8,10 @@ from borgmatic.execute import execute_command
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 
 
-def make_prune_flags(config, local_borg_version):
+def make_prune_flags(config, prune_arguments, local_borg_version):
     '''
     '''
-    Given a configuration dict mapping from option name to value, transform it into an sequence of
+    Given a configuration dict mapping from option name to value, prune arguments as an
+    argparse.Namespace instance, and the local Borg version, produce a corresponding sequence of
     command-line flags.
     command-line flags.
 
 
     For example, given a retention config of:
     For example, given a retention config of:
@@ -40,7 +41,7 @@ def make_prune_flags(config, local_borg_version):
         if prefix
         if prefix
         else (
         else (
             flags.make_match_archives_flags(
             flags.make_match_archives_flags(
-                config.get('match_archives'),
+                prune_arguments.match_archives or config.get('match_archives'),
                 config.get('archive_name_format'),
                 config.get('archive_name_format'),
                 local_borg_version,
                 local_borg_version,
             )
             )
@@ -69,7 +70,7 @@ def prune_archives(
 
 
     full_command = (
     full_command = (
         (local_path, 'prune')
         (local_path, 'prune')
-        + make_prune_flags(config, local_borg_version)
+        + make_prune_flags(config, prune_arguments, 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 ())
         + (('--log-json',) if global_arguments.log_json else ())
         + (('--log-json',) if global_arguments.log_json else ())
@@ -78,7 +79,7 @@ def prune_archives(
         + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
         + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
         + flags.make_flags_from_arguments(
         + flags.make_flags_from_arguments(
             prune_arguments,
             prune_arguments,
-            excludes=('repository', 'stats', 'list_archives'),
+            excludes=('repository', 'match_archives', 'stats', 'list_archives'),
         )
         )
         + (('--list',) if prune_arguments.list_archives else ())
         + (('--list',) if prune_arguments.list_archives else ())
         + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
         + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())

+ 32 - 15
borgmatic/commands/arguments.py

@@ -469,7 +469,7 @@ def make_parsers():
     )
     )
     transfer_group.add_argument(
     transfer_group.add_argument(
         '--archive',
         '--archive',
-        help='Name of single archive to transfer (or "latest"), defaults to transferring all archives',
+        help='Name or hash of a single archive to transfer (or "latest"), defaults to transferring all archives',
     )
     )
     transfer_group.add_argument(
     transfer_group.add_argument(
         '--upgrader',
         '--upgrader',
@@ -486,7 +486,7 @@ def make_parsers():
         '--match-archives',
         '--match-archives',
         '--glob-archives',
         '--glob-archives',
         metavar='PATTERN',
         metavar='PATTERN',
-        help='Only transfer archives with names matching this pattern',
+        help='Only transfer archives with names, hashes, or series matching this pattern',
     )
     )
     transfer_group.add_argument(
     transfer_group.add_argument(
         '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
         '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
@@ -535,6 +535,13 @@ def make_parsers():
         '--repository',
         '--repository',
         help='Path of specific existing repository to prune (must be already specified in a borgmatic configuration file), quoted globs supported',
         help='Path of specific existing repository to prune (must be already specified in a borgmatic configuration file), quoted globs supported',
     )
     )
+    prune_group.add_argument(
+        '-a',
+        '--match-archives',
+        '--glob-archives',
+        metavar='PATTERN',
+        help='When pruning, only consider archives with names, hashes, or series matching this pattern',
+    )
     prune_group.add_argument(
     prune_group.add_argument(
         '--stats',
         '--stats',
         dest='stats',
         dest='stats',
@@ -673,7 +680,7 @@ def make_parsers():
         '--match-archives',
         '--match-archives',
         '--glob-archives',
         '--glob-archives',
         metavar='PATTERN',
         metavar='PATTERN',
-        help='Only check archives with names matching this pattern',
+        help='Only check archives with names, hashes, or series matching this pattern',
     )
     )
     check_group.add_argument(
     check_group.add_argument(
         '--only',
         '--only',
@@ -705,7 +712,7 @@ def make_parsers():
     )
     )
     delete_group.add_argument(
     delete_group.add_argument(
         '--archive',
         '--archive',
-        help='Archive to delete',
+        help='Archive name, hash, or series to delete',
     )
     )
     delete_group.add_argument(
     delete_group.add_argument(
         '--list',
         '--list',
@@ -749,7 +756,7 @@ def make_parsers():
         '--match-archives',
         '--match-archives',
         '--glob-archives',
         '--glob-archives',
         metavar='PATTERN',
         metavar='PATTERN',
-        help='Only delete archives matching this pattern',
+        help='Only delete archives with names, hashes, or series matching this pattern',
     )
     )
     delete_group.add_argument(
     delete_group.add_argument(
         '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
         '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
@@ -795,7 +802,7 @@ def make_parsers():
         help='Path of repository to extract, defaults to the configured repository if there is only one, quoted globs supported',
         help='Path of repository to extract, defaults to the configured repository if there is only one, quoted globs supported',
     )
     )
     extract_group.add_argument(
     extract_group.add_argument(
-        '--archive', help='Name of archive to extract (or "latest")', required=True
+        '--archive', help='Name or hash of a single archive to extract (or "latest")', required=True
     )
     )
     extract_group.add_argument(
     extract_group.add_argument(
         '--path',
         '--path',
@@ -863,7 +870,7 @@ def make_parsers():
     )
     )
     config_bootstrap_group.add_argument(
     config_bootstrap_group.add_argument(
         '--archive',
         '--archive',
-        help='Name of archive to extract config files from, defaults to "latest"',
+        help='Name or hash of a single archive to extract config files from, defaults to "latest"',
         default='latest',
         default='latest',
     )
     )
     config_bootstrap_group.add_argument(
     config_bootstrap_group.add_argument(
@@ -955,7 +962,7 @@ def make_parsers():
         help='Path of repository to export from, defaults to the configured repository if there is only one, quoted globs supported',
         help='Path of repository to export from, defaults to the configured repository if there is only one, quoted globs supported',
     )
     )
     export_tar_group.add_argument(
     export_tar_group.add_argument(
-        '--archive', help='Name of archive to export (or "latest")', required=True
+        '--archive', help='Name or hash of a single archive to export (or "latest")', required=True
     )
     )
     export_tar_group.add_argument(
     export_tar_group.add_argument(
         '--path',
         '--path',
@@ -1000,7 +1007,9 @@ def make_parsers():
         '--repository',
         '--repository',
         help='Path of repository to use, defaults to the configured repository if there is only one, quoted globs supported',
         help='Path of repository to use, defaults to the configured repository if there is only one, quoted globs supported',
     )
     )
-    mount_group.add_argument('--archive', help='Name of archive to mount (or "latest")')
+    mount_group.add_argument(
+        '--archive', help='Name or hash of a single archive to mount (or "latest")'
+    )
     mount_group.add_argument(
     mount_group.add_argument(
         '--mount-point',
         '--mount-point',
         metavar='PATH',
         metavar='PATH',
@@ -1120,7 +1129,9 @@ def make_parsers():
         help='Path of repository to restore from, defaults to the configured repository if there is only one, quoted globs supported',
         help='Path of repository to restore from, defaults to the configured repository if there is only one, quoted globs supported',
     )
     )
     restore_group.add_argument(
     restore_group.add_argument(
-        '--archive', help='Name of archive to restore from (or "latest")', required=True
+        '--archive',
+        help='Name or hash of a single archive to restore from (or "latest")',
+        required=True,
     )
     )
     restore_group.add_argument(
     restore_group.add_argument(
         '--data-source',
         '--data-source',
@@ -1188,7 +1199,7 @@ def make_parsers():
         '--match-archives',
         '--match-archives',
         '--glob-archives',
         '--glob-archives',
         metavar='PATTERN',
         metavar='PATTERN',
-        help='Only list archive names matching this pattern',
+        help='Only list archive names, hashes, or series matching this pattern',
     )
     )
     repo_list_group.add_argument(
     repo_list_group.add_argument(
         '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
         '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
@@ -1235,7 +1246,9 @@ def make_parsers():
         '--repository',
         '--repository',
         help='Path of repository containing archive to list, defaults to the configured repositories, quoted globs supported',
         help='Path of repository containing archive to list, defaults to the configured repositories, quoted globs supported',
     )
     )
-    list_group.add_argument('--archive', help='Name of the archive to list (or "latest")')
+    list_group.add_argument(
+        '--archive', help='Name or hash of a single archive to list (or "latest")'
+    )
     list_group.add_argument(
     list_group.add_argument(
         '--path',
         '--path',
         metavar='PATH',
         metavar='PATH',
@@ -1321,7 +1334,9 @@ def make_parsers():
         '--repository',
         '--repository',
         help='Path of repository containing archive to show info for, defaults to the configured repository if there is only one, quoted globs supported',
         help='Path of repository containing archive to show info for, defaults to the configured repository if there is only one, quoted globs supported',
     )
     )
-    info_group.add_argument('--archive', help='Name of archive to show info for (or "latest")')
+    info_group.add_argument(
+        '--archive', help='Archive name, hash, or series to show info for (or "latest")'
+    )
     info_group.add_argument(
     info_group.add_argument(
         '--json', dest='json', default=False, action='store_true', help='Output results as JSON'
         '--json', dest='json', default=False, action='store_true', help='Output results as JSON'
     )
     )
@@ -1335,7 +1350,7 @@ def make_parsers():
         '--match-archives',
         '--match-archives',
         '--glob-archives',
         '--glob-archives',
         metavar='PATTERN',
         metavar='PATTERN',
-        help='Only show info for archive names matching this pattern',
+        help='Only show info for archive names, hashes, or series matching this pattern',
     )
     )
     info_group.add_argument(
     info_group.add_argument(
         '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
         '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
@@ -1460,7 +1475,9 @@ def make_parsers():
         '--repository',
         '--repository',
         help='Path of repository to pass to Borg, defaults to the configured repositories, quoted globs supported',
         help='Path of repository to pass to Borg, defaults to the configured repositories, quoted globs supported',
     )
     )
-    borg_group.add_argument('--archive', help='Name of archive to pass to Borg (or "latest")')
+    borg_group.add_argument(
+        '--archive', help='Archive name, hash, or series to pass to Borg (or "latest")'
+    )
     borg_group.add_argument(
     borg_group.add_argument(
         '--',
         '--',
         metavar='OPTION',
         metavar='OPTION',

+ 8 - 5
borgmatic/config/schema.yaml

@@ -397,11 +397,14 @@ properties:
     archive_name_format:
     archive_name_format:
         type: string
         type: string
         description: |
         description: |
-            Name of the archive. Borg placeholders can be used. See the output
-            of "borg help placeholders" for details. Defaults to
-            "{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}". When running actions like
-            repo-list, info, or check, borgmatic automatically tries to match
-            only archives created with this name format.
+            Name of the archive to create. Borg placeholders can be used. See
+            the output of "borg help placeholders" for details. Defaults to
+            "{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}" with Borg 1 and
+            "{hostname}" with Borg 2, as Borg 2 does not require unique
+            archive names; identical archive names form a common "series" that
+            can be targeted together. When running actions like repo-list,
+            info, or check, borgmatic automatically tries to match only
+            archives created with this name format.
         example: "{hostname}-documents-{now}"
         example: "{hostname}-documents-{now}"
     match_archives:
     match_archives:
         type: string
         type: string

+ 1 - 1
docs/docker-compose.yaml

@@ -11,7 +11,7 @@ services:
         ENVIRONMENT: development
         ENVIRONMENT: development
   message:
   message:
     image: alpine
     image: alpine
-    container_name: message
+    container_name: borgmatic-docs-message
     command:
     command:
       - sh
       - sh
       - -c
       - -c

+ 4 - 4
docs/how-to/backup-your-databases.md

@@ -336,15 +336,15 @@ borgmatic restore --archive host-2023-01-02T04:06:07.080910
 
 
 (No borgmatic `restore` action? Upgrade borgmatic!)
 (No borgmatic `restore` action? Upgrade borgmatic!)
 
 
-With newer versions of borgmatic, you can simplify this to:
+Or you can simplify this to:
 
 
 ```bash
 ```bash
 borgmatic restore --archive latest
 borgmatic restore --archive latest
 ```
 ```
 
 
-The `--archive` value is the name of the archive to restore from. This
-restores all databases dumps that borgmatic originally backed up to that
-archive.
+The `--archive` value is the name of the archive or archive hash to restore
+from. This restores all databases dumps that borgmatic originally backed up to
+that archive.
 
 
 This is a destructive action! `borgmatic restore` replaces live databases by
 This is a destructive action! `borgmatic restore` replaces live databases by
 restoring dumps from the selected archive. So be very careful when and where
 restoring dumps from the selected archive. So be very careful when and where

+ 12 - 4
docs/how-to/extract-a-backup.md

@@ -40,10 +40,10 @@ Or simplify this to:
 borgmatic extract --archive latest
 borgmatic extract --archive latest
 ```
 ```
 
 
-The `--archive` value is the name of the archive to extract. This extracts the
-entire contents of the archive to the current directory, so make sure you're
-in the right place before running the command—or see below about the
-`--destination` flag.
+The `--archive` value is the name of the archive or archive hash to extract.
+This extracts the entire contents of the archive to the current directory, so
+make sure you're in the right place before running the command—or see below
+about the `--destination` flag.
 
 
 ## Repository selection
 ## Repository selection
 
 
@@ -131,6 +131,14 @@ Or use the "latest" value for the archive to mount the latest archive:
 borgmatic mount --archive latest --mount-point /mnt
 borgmatic mount --archive latest --mount-point /mnt
 ```
 ```
 
 
+<span class="minilink minilink-addedin">With Borg version 2.x</span>You can
+provide a series name for the `--archive` value to mount multiple archives in
+that series:
+
+```bash
+borgmatic mount --archive seriesname --mount-point /mnt
+```
+
 If you'd like to restrict the mounted filesystem to only particular paths from
 If you'd like to restrict the mounted filesystem to only particular paths from
 your archive, use the `--path` flag, similar to the `extract` action above.
 your archive, use the `--path` flag, similar to the `extract` action above.
 For instance:
 For instance:

+ 11 - 4
docs/how-to/make-per-application-backups.md

@@ -82,10 +82,14 @@ this option in the `storage:` section of your configuration.
 
 
 This example means that when borgmatic creates an archive, its name will start
 This example means that when borgmatic creates an archive, its name will start
 with the string `home-directories-` and end with a timestamp for its creation
 with the string `home-directories-` and end with a timestamp for its creation
-time. If `archive_name_format` is unspecified, the default is
+time. If `archive_name_format` is unspecified, the default with Borg 1 is
 `{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}`, meaning your system hostname plus a
 `{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}`, meaning your system hostname plus a
 timestamp in a particular format.
 timestamp in a particular format.
 
 
+<span class="minilink minilink-addedin">With Borg version 2.x</span>The default
+is just `{hostname}`, as Borg 2 does not require unique archive names; identical
+archive names form a common "series" that can be targeted together.
+
 
 
 ### Archive filtering
 ### Archive filtering
 
 
@@ -129,10 +133,13 @@ archive_name_format: {hostname}-user-data-{now}
 match_archives: sh:myhost-user-data-*        
 match_archives: sh:myhost-user-data-*        
 ```
 ```
 
 
-For Borg 1.x, use a shell pattern for the `match_archives` value and see the
-[Borg patterns
+<span class="minilink minilink-addedin">With Borg version 1.x</span>Use a shell
+pattern for the `match_archives` value and see the [Borg patterns
 documentation](https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-help-patterns)
 documentation](https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-help-patterns)
-for more information. For Borg 2.x, see the [match archives
+for more information.
+
+<span class="minilink minilink-addedin">With Borg version 2.x</span>See the
+[match archives
 documentation](https://borgbackup.readthedocs.io/en/2.0.0b12/usage/help.html#borg-help-match-archives).
 documentation](https://borgbackup.readthedocs.io/en/2.0.0b12/usage/help.html#borg-help-match-archives).
 
 
 Some borgmatic command-line actions also have a `--match-archives` flag that
 Some borgmatic command-line actions also have a `--match-archives` flag that

+ 101 - 2
tests/unit/borg/test_create.py

@@ -581,6 +581,9 @@ def test_make_base_create_command_includes_patterns_file_in_borg_command():
         None
         None
     )
     )
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('ensure_files_readable')
     pattern_flags = ('--patterns-from', mock_pattern_file.name)
     pattern_flags = ('--patterns-from', mock_pattern_file.name)
@@ -631,6 +634,9 @@ def test_make_base_create_command_includes_sources_and_config_paths_in_borg_comm
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('make_pattern_flags').and_return(())
     flexmock(module).should_receive('make_pattern_flags').and_return(())
@@ -676,6 +682,9 @@ def test_make_base_create_command_with_store_config_false_omits_config_files():
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('make_pattern_flags').and_return(())
     flexmock(module).should_receive('make_pattern_flags').and_return(())
@@ -719,6 +728,9 @@ def test_make_base_create_command_includes_exclude_patterns_in_borg_command():
     mock_exclude_file = flexmock(name='/tmp/excludes')
     mock_exclude_file = flexmock(name='/tmp/excludes')
     flexmock(module).should_receive('write_pattern_file').and_return(mock_exclude_file)
     flexmock(module).should_receive('write_pattern_file').and_return(mock_exclude_file)
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('make_pattern_flags').and_return(())
     flexmock(module).should_receive('make_pattern_flags').and_return(())
@@ -795,6 +807,9 @@ def test_make_base_create_command_includes_configuration_option_as_command_flag(
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
     flexmock(module.feature).should_receive('available').and_return(feature_available)
     flexmock(module.feature).should_receive('available').and_return(feature_available)
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('make_pattern_flags').and_return(())
     flexmock(module).should_receive('make_pattern_flags').and_return(())
@@ -837,6 +852,9 @@ def test_make_base_create_command_includes_dry_run_in_borg_command():
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('make_pattern_flags').and_return(())
     flexmock(module).should_receive('make_pattern_flags').and_return(())
@@ -879,6 +897,9 @@ def test_make_base_create_command_includes_local_path_in_borg_command():
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('make_pattern_flags').and_return(())
     flexmock(module).should_receive('make_pattern_flags').and_return(())
@@ -921,6 +942,9 @@ def test_make_base_create_command_includes_remote_path_in_borg_command():
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('make_pattern_flags').and_return(())
     flexmock(module).should_receive('make_pattern_flags').and_return(())
@@ -963,6 +987,9 @@ def test_make_base_create_command_includes_log_json_in_borg_command():
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('make_pattern_flags').and_return(())
     flexmock(module).should_receive('make_pattern_flags').and_return(())
@@ -1004,6 +1031,9 @@ def test_make_base_create_command_includes_list_flags_in_borg_command():
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('make_pattern_flags').and_return(())
     flexmock(module).should_receive('make_pattern_flags').and_return(())
@@ -1046,6 +1076,9 @@ def test_make_base_create_command_with_stream_processes_ignores_read_special_fal
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('make_pattern_flags').and_return(())
     flexmock(module).should_receive('make_pattern_flags').and_return(())
@@ -1095,6 +1128,9 @@ def test_make_base_create_command_with_stream_processes_and_read_special_true_sk
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('make_pattern_flags').and_return(())
     flexmock(module).should_receive('make_pattern_flags').and_return(())
@@ -1141,6 +1177,9 @@ def test_make_base_create_command_with_non_matching_source_directories_glob_pass
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('make_pattern_flags').and_return(())
     flexmock(module).should_receive('make_pattern_flags').and_return(())
@@ -1183,6 +1222,9 @@ def test_make_base_create_command_expands_glob_in_source_directories():
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('make_pattern_flags').and_return(())
     flexmock(module).should_receive('make_pattern_flags').and_return(())
@@ -1225,6 +1267,9 @@ def test_make_base_create_command_includes_archive_name_format_in_borg_command()
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('make_pattern_flags').and_return(())
     flexmock(module).should_receive('make_pattern_flags').and_return(())
@@ -1255,7 +1300,52 @@ def test_make_base_create_command_includes_archive_name_format_in_borg_command()
     assert not exclude_file
     assert not exclude_file
 
 
 
 
-def test_base_create_command_includes_archive_name_format_with_placeholders_in_borg_command():
+def test_make_base_create_command_includes_default_archive_name_format_in_borg_command():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
+    flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
+    flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
+    flexmock(module).should_receive('map_directories_to_devices').and_return({})
+    flexmock(module).should_receive('expand_directories').and_return(())
+    flexmock(module).should_receive('pattern_root_directories').and_return([])
+    flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
+    flexmock(module).should_receive('expand_home_directories').and_return(())
+    flexmock(module).should_receive('write_pattern_file').and_return(None)
+    flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
+    flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module).should_receive('ensure_files_readable')
+    flexmock(module).should_receive('make_pattern_flags').and_return(())
+    flexmock(module).should_receive('make_exclude_flags').and_return(())
+    flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
+        ('repo::{hostname}',)
+    )
+
+    (create_flags, create_positional_arguments, pattern_file, exclude_file) = (
+        module.make_base_create_command(
+            dry_run=False,
+            repository_path='repo',
+            config={
+                'source_directories': ['foo', 'bar'],
+                'repositories': ['repo'],
+            },
+            config_paths=['/tmp/test.yaml'],
+            local_borg_version='1.2.3',
+            global_arguments=flexmock(log_json=False),
+            borgmatic_source_directories=(),
+        )
+    )
+
+    assert create_flags == ('borg', 'create')
+    assert create_positional_arguments == ('repo::{hostname}', 'foo', 'bar')
+    assert not pattern_file
+    assert not exclude_file
+
+
+def test_make_base_create_command_includes_archive_name_format_with_placeholders_in_borg_command():
     repository_archive_pattern = 'repo::Documents_{hostname}-{now}'  # noqa: FS003
     repository_archive_pattern = 'repo::Documents_{hostname}-{now}'  # noqa: FS003
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1265,6 +1355,9 @@ def test_base_create_command_includes_archive_name_format_with_placeholders_in_b
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('make_pattern_flags').and_return(())
     flexmock(module).should_receive('make_pattern_flags').and_return(())
@@ -1295,7 +1388,7 @@ def test_base_create_command_includes_archive_name_format_with_placeholders_in_b
     assert not exclude_file
     assert not exclude_file
 
 
 
 
-def test_base_create_command_includes_repository_and_archive_name_format_with_placeholders_in_borg_command():
+def test_make_base_create_command_includes_repository_and_archive_name_format_with_placeholders_in_borg_command():
     repository_archive_pattern = '{fqdn}::Documents_{hostname}-{now}'  # noqa: FS003
     repository_archive_pattern = '{fqdn}::Documents_{hostname}-{now}'  # noqa: FS003
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1305,6 +1398,9 @@ def test_base_create_command_includes_repository_and_archive_name_format_with_pl
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('make_pattern_flags').and_return(())
     flexmock(module).should_receive('make_pattern_flags').and_return(())
@@ -1347,6 +1443,9 @@ def test_make_base_create_command_includes_extra_borg_options_in_borg_command():
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('write_pattern_file').and_return(None)
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
     flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
+    flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
+        '{hostname}'
+    )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('ensure_files_readable')
     flexmock(module).should_receive('make_pattern_flags').and_return(())
     flexmock(module).should_receive('make_pattern_flags').and_return(())

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

@@ -85,6 +85,24 @@ def test_make_repository_archive_flags_with_borg_features_joins_repository_and_a
     ) == ('repo::archive',)
     ) == ('repo::archive',)
 
 
 
 
+def test_get_default_archive_name_format_with_archive_series_feature_uses_series_archive_name_format():
+    flexmock(module.feature).should_receive('available').and_return(True)
+
+    assert (
+        module.get_default_archive_name_format(local_borg_version='1.2.3')
+        == module.DEFAULT_ARCHIVE_NAME_FORMAT_WITH_SERIES
+    )
+
+
+def test_get_default_archive_name_format_without_archive_series_feature_uses_non_series_archive_name_format():
+    flexmock(module.feature).should_receive('available').and_return(False)
+
+    assert (
+        module.get_default_archive_name_format(local_borg_version='1.2.3')
+        == module.DEFAULT_ARCHIVE_NAME_FORMAT_WITHOUT_SERIES
+    )
+
+
 @pytest.mark.parametrize(
 @pytest.mark.parametrize(
     'match_archives,archive_name_format,feature_available,expected_result',
     'match_archives,archive_name_format,feature_available,expected_result',
     (
     (
@@ -175,12 +193,27 @@ def test_make_repository_archive_flags_with_borg_features_joins_repository_and_a
             True,
             True,
             (),
             (),
         ),
         ),
+        (
+            'abcdefabcdef',
+            None,
+            True,
+            ('--match-archives', 'aid:abcdefabcdef'),
+        ),
+        (
+            'aid:abcdefabcdef',
+            None,
+            True,
+            ('--match-archives', 'aid:abcdefabcdef'),
+        ),
     ),
     ),
 )
 )
 def test_make_match_archives_flags_makes_flags_with_globs(
 def test_make_match_archives_flags_makes_flags_with_globs(
     match_archives, archive_name_format, feature_available, expected_result
     match_archives, archive_name_format, feature_available, expected_result
 ):
 ):
     flexmock(module.feature).should_receive('available').and_return(feature_available)
     flexmock(module.feature).should_receive('available').and_return(feature_available)
+    flexmock(module).should_receive('get_default_archive_name_format').and_return(
+        module.DEFAULT_ARCHIVE_NAME_FORMAT_WITHOUT_SERIES
+    )
 
 
     assert (
     assert (
         module.make_match_archives_flags(
         module.make_match_archives_flags(

+ 71 - 7
tests/unit/borg/test_prune.py

@@ -36,7 +36,9 @@ def test_make_prune_flags_returns_flags_from_config():
     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(())
     flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    result = module.make_prune_flags(config, local_borg_version='1.2.3')
+    result = module.make_prune_flags(
+        config, flexmock(match_archives=None), local_borg_version='1.2.3'
+    )
 
 
     assert result == BASE_PRUNE_FLAGS
     assert result == BASE_PRUNE_FLAGS
 
 
@@ -49,7 +51,9 @@ def test_make_prune_flags_accepts_prefix_with_placeholders():
     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(())
     flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    result = module.make_prune_flags(config, local_borg_version='1.2.3')
+    result = module.make_prune_flags(
+        config, flexmock(match_archives=None), local_borg_version='1.2.3'
+    )
 
 
     expected = (
     expected = (
         '--keep-daily',
         '--keep-daily',
@@ -69,7 +73,9 @@ def test_make_prune_flags_with_prefix_without_borg_features_uses_glob_archives()
     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(())
     flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    result = module.make_prune_flags(config, local_borg_version='1.2.3')
+    result = module.make_prune_flags(
+        config, flexmock(match_archives=None), local_borg_version='1.2.3'
+    )
 
 
     expected = (
     expected = (
         '--keep-daily',
         '--keep-daily',
@@ -90,7 +96,9 @@ def test_make_prune_flags_prefers_prefix_to_archive_name_format():
     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').never()
     flexmock(module.flags).should_receive('make_match_archives_flags').never()
 
 
-    result = module.make_prune_flags(config, local_borg_version='1.2.3')
+    result = module.make_prune_flags(
+        config, flexmock(match_archives=None), local_borg_version='1.2.3'
+    )
 
 
     expected = (
     expected = (
         '--keep-daily',
         '--keep-daily',
@@ -111,9 +119,63 @@ def test_make_prune_flags_without_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(
     flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
         None, 'bar-{now}', '1.2.3'  # noqa: FS003
         None, 'bar-{now}', '1.2.3'  # noqa: FS003
-    ).and_return(('--match-archives', 'sh:bar-*'))
+    ).and_return(('--match-archives', 'sh:bar-*')).once()
+
+    result = module.make_prune_flags(
+        config, flexmock(match_archives=None), local_borg_version='1.2.3'
+    )
+
+    expected = (
+        '--keep-daily',
+        '1',
+        '--match-archives',
+        'sh:bar-*',  # noqa: FS003
+    )
+
+    assert result == expected
+
+
+def test_make_prune_flags_without_prefix_uses_match_archives_flag_instead_of_option():
+    config = {
+        'archive_name_format': 'bar-{now}',  # noqa: FS003
+        'match_archives': 'foo*',
+        'keep_daily': 1,
+        'prefix': None,
+    }
+    flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        'baz*', 'bar-{now}', '1.2.3'  # noqa: FS003
+    ).and_return(('--match-archives', 'sh:bar-*')).once()
 
 
-    result = module.make_prune_flags(config, local_borg_version='1.2.3')
+    result = module.make_prune_flags(
+        config, flexmock(match_archives='baz*'), local_borg_version='1.2.3'
+    )
+
+    expected = (
+        '--keep-daily',
+        '1',
+        '--match-archives',
+        'sh:bar-*',  # noqa: FS003
+    )
+
+    assert result == expected
+
+
+def test_make_prune_flags_without_prefix_uses_match_archives_option():
+    config = {
+        'archive_name_format': 'bar-{now}',  # noqa: FS003
+        'match_archives': 'foo*',
+        'keep_daily': 1,
+        'prefix': None,
+    }
+    flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        'foo*', 'bar-{now}', '1.2.3'  # noqa: FS003
+    ).and_return(('--match-archives', 'sh:bar-*')).once()
+
+    result = module.make_prune_flags(
+        config, flexmock(match_archives=None), local_borg_version='1.2.3'
+    )
 
 
     expected = (
     expected = (
         '--keep-daily',
         '--keep-daily',
@@ -133,7 +195,9 @@ def test_make_prune_flags_ignores_keep_exclude_tags_in_config():
     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(())
     flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    result = module.make_prune_flags(config, local_borg_version='1.2.3')
+    result = module.make_prune_flags(
+        config, flexmock(match_archives=None), local_borg_version='1.2.3'
+    )
 
 
     assert result == ('--keep-daily', '1')
     assert result == ('--keep-daily', '1')