浏览代码

Update list action for Borg 2 support, add rinfo action, and update extract consistency check for Borg 2.

Dan Helfman 2 年之前
父节点
当前提交
cc04bf57df

+ 1 - 1
borgmatic/borg/check.py

@@ -323,6 +323,6 @@ def check_archives(
 
 
     if 'extract' in checks:
     if 'extract' in checks:
         extract.extract_last_archive_dry_run(
         extract.extract_last_archive_dry_run(
-            storage_config, repository, lock_wait, local_path, remote_path
+            storage_config, local_borg_version, repository, lock_wait, local_path, remote_path
         )
         )
         write_check_time(make_check_time_path(location_config, borg_repository_id, 'extract'))
         write_check_time(make_check_time_path(location_config, borg_repository_id, 'extract'))

+ 14 - 26
borgmatic/borg/extract.py

@@ -2,14 +2,19 @@ import logging
 import os
 import os
 import subprocess
 import subprocess
 
 
-from borgmatic.borg import environment, feature, flags
+from borgmatic.borg import environment, feature, flags, rlist
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 
 
 def extract_last_archive_dry_run(
 def extract_last_archive_dry_run(
-    storage_config, repository, lock_wait=None, local_path='borg', remote_path=None
+    storage_config,
+    local_borg_version,
+    repository,
+    lock_wait=None,
+    local_path='borg',
+    remote_path=None,
 ):
 ):
     '''
     '''
     Perform an extraction dry-run of the most recent archive. If there are no archives, skip the
     Perform an extraction dry-run of the most recent archive. If there are no archives, skip the
@@ -23,40 +28,23 @@ def extract_last_archive_dry_run(
     elif logger.isEnabledFor(logging.INFO):
     elif logger.isEnabledFor(logging.INFO):
         verbosity_flags = ('--info',)
         verbosity_flags = ('--info',)
 
 
-    full_list_command = (
-        (local_path, 'list', '--short')
-        + remote_path_flags
-        + lock_wait_flags
-        + verbosity_flags
-        + (repository,)
-    )
-
-    borg_environment = environment.make_environment(storage_config)
-
-    list_output = execute_command(
-        full_list_command,
-        output_log_level=None,
-        borg_local_path=local_path,
-        extra_environment=borg_environment,
-    )
-
     try:
     try:
-        last_archive_name = list_output.strip().splitlines()[-1]
-    except IndexError:
+        last_archive_name = rlist.resolve_archive_name(
+            repository, 'latest', storage_config, local_borg_version, local_path, remote_path
+        )
+    except ValueError:
+        logger.warning('No archives found. Skipping extract consistency check.')
         return
         return
 
 
     list_flag = ('--list',) if logger.isEnabledFor(logging.DEBUG) else ()
     list_flag = ('--list',) if logger.isEnabledFor(logging.DEBUG) else ()
+    borg_environment = environment.make_environment(storage_config)
     full_extract_command = (
     full_extract_command = (
         (local_path, 'extract', '--dry-run')
         (local_path, 'extract', '--dry-run')
         + remote_path_flags
         + remote_path_flags
         + lock_wait_flags
         + lock_wait_flags
         + verbosity_flags
         + verbosity_flags
         + list_flag
         + list_flag
-        + (
-            '{repository}::{last_archive_name}'.format(
-                repository=repository, last_archive_name=last_archive_name
-            ),
-        )
+        + flags.make_repository_archive_flags(repository, last_archive_name, local_borg_version)
     )
     )
 
 
     execute_command(
     execute_command(

+ 82 - 68
borgmatic/borg/list.py

@@ -1,58 +1,24 @@
+import argparse
 import copy
 import copy
 import logging
 import logging
 import re
 import re
 
 
-from borgmatic.borg import environment
-from borgmatic.borg.flags import make_flags, make_flags_from_arguments
+from borgmatic.borg import environment, feature, flags, rlist
 from borgmatic.execute import execute_command
 from borgmatic.execute import execute_command
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 
 
-def resolve_archive_name(repository, archive, storage_config, local_path='borg', remote_path=None):
-    '''
-    Given a local or remote repository path, an archive name, a storage config dict, a local Borg
-    path, and a remote Borg path, simply return the archive name. But if the archive name is
-    "latest", then instead introspect the repository for the latest archive and return its name.
-
-    Raise ValueError if "latest" is given but there are no archives in the repository.
-    '''
-    if archive != "latest":
-        return archive
-
-    lock_wait = storage_config.get('lock_wait', None)
-
-    full_command = (
-        (local_path, 'list')
-        + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
-        + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
-        + make_flags('remote-path', remote_path)
-        + make_flags('lock-wait', lock_wait)
-        + make_flags('last', 1)
-        + ('--short', repository)
-    )
-
-    output = execute_command(
-        full_command,
-        output_log_level=None,
-        borg_local_path=local_path,
-        extra_environment=environment.make_environment(storage_config),
-    )
-    try:
-        latest_archive = output.strip().splitlines()[-1]
-    except IndexError:
-        raise ValueError('No archives found in the repository')
-
-    logger.debug('{}: Latest archive is {}'.format(repository, latest_archive))
-
-    return latest_archive
-
-
 MAKE_FLAGS_EXCLUDES = ('repository', 'archive', 'successful', 'paths', 'find_paths')
 MAKE_FLAGS_EXCLUDES = ('repository', 'archive', 'successful', 'paths', 'find_paths')
 
 
 
 
 def make_list_command(
 def make_list_command(
-    repository, storage_config, list_arguments, local_path='borg', remote_path=None
+    repository,
+    storage_config,
+    local_borg_version,
+    list_arguments,
+    local_path='borg',
+    remote_path=None,
 ):
 ):
     '''
     '''
     Given a local or remote repository path, a storage config dict, the arguments to the list
     Given a local or remote repository path, a storage config dict, the arguments to the list
@@ -73,13 +39,15 @@ def make_list_command(
             if logger.isEnabledFor(logging.DEBUG) and not list_arguments.json
             if logger.isEnabledFor(logging.DEBUG) and not list_arguments.json
             else ()
             else ()
         )
         )
-        + make_flags('remote-path', remote_path)
-        + make_flags('lock-wait', lock_wait)
-        + make_flags_from_arguments(list_arguments, excludes=MAKE_FLAGS_EXCLUDES,)
+        + flags.make_flags('remote-path', remote_path)
+        + flags.make_flags('lock-wait', lock_wait)
+        + flags.make_flags_from_arguments(list_arguments, excludes=MAKE_FLAGS_EXCLUDES,)
         + (
         + (
-            ('::'.join((repository, list_arguments.archive)),)
+            flags.make_repository_archive_flags(
+                repository, list_arguments.archive, local_borg_version
+            )
             if list_arguments.archive
             if list_arguments.archive
-            else (repository,)
+            else flags.make_repository_flags(repository, local_borg_version)
         )
         )
         + (tuple(list_arguments.paths) if list_arguments.paths else ())
         + (tuple(list_arguments.paths) if list_arguments.paths else ())
     )
     )
@@ -109,29 +77,76 @@ def make_find_paths(find_paths):
     )
     )
 
 
 
 
-def list_archives(repository, storage_config, list_arguments, local_path='borg', remote_path=None):
+def list_archive(
+    repository,
+    storage_config,
+    local_borg_version,
+    list_arguments,
+    local_path='borg',
+    remote_path=None,
+):
     '''
     '''
-    Given a local or remote repository path, a storage config dict, the arguments to the list
-    action, and local and remote Borg paths, display the output of listing Borg archives in the
-    repository or return JSON output. Or, if an archive name is given, list the files in that
-    archive. Or, if list_arguments.find_paths are given, list the files by searching across multiple
-    archives.
+    Given a local or remote repository path, a storage config dict, the local Borg version, the
+    arguments to the list action, and local and remote Borg paths, display the output of listing
+    the files of a Borg archive (or return JSON output). If list_arguments.find_paths are given,
+    list the files by searching across multiple archives. If neither find_paths nor archive name
+    are given, instead list the archives in the given repository.
     '''
     '''
+    if not list_arguments.archive and not list_arguments.find_paths:
+        if feature.available(feature.Feature.RLIST, local_borg_version):
+            logger.warning(
+                'Omitting the --archive flag on the list action is deprecated when using Borg 2.x. Use the rlist action instead.'
+            )
+
+        rlist_arguments = argparse.Namespace(
+            repository=repository,
+            short=list_arguments.short,
+            format=list_arguments.format,
+            json=list_arguments.json,
+            prefix=list_arguments.prefix,
+            glob_archives=list_arguments.glob_archives,
+            sort_by=list_arguments.sort_by,
+            first=list_arguments.first,
+            last=list_arguments.last,
+        )
+        return rlist.list_repository(
+            repository, storage_config, local_borg_version, rlist_arguments, local_path, remote_path
+        )
+
+    if feature.available(feature.Feature.RLIST, local_borg_version):
+        for flag_name in ('prefix', 'glob-archives', 'sort-by', 'first', 'last'):
+            if getattr(list_arguments, flag_name.replace('-', '_'), None):
+                raise ValueError(
+                    f'The --{flag_name} flag on the list action is not supported when using the --archive flag and Borg 2.x.'
+                )
+
     borg_environment = environment.make_environment(storage_config)
     borg_environment = environment.make_environment(storage_config)
 
 
     # If there are any paths to find (and there's not a single archive already selected), start by
     # If there are any paths to find (and there's not a single archive already selected), start by
     # getting a list of archives to search.
     # getting a list of archives to search.
     if list_arguments.find_paths and not list_arguments.archive:
     if list_arguments.find_paths and not list_arguments.archive:
-        repository_arguments = copy.copy(list_arguments)
-        repository_arguments.archive = None
-        repository_arguments.json = False
-        repository_arguments.format = None
+        rlist_arguments = argparse.Namespace(
+            repository=repository,
+            short=True,
+            format=None,
+            json=None,
+            prefix=list_arguments.prefix,
+            glob_archives=list_arguments.glob_archives,
+            sort_by=list_arguments.sort_by,
+            first=list_arguments.first,
+            last=list_arguments.last,
+        )
 
 
         # Ask Borg to list archives. Capture its output for use below.
         # Ask Borg to list archives. Capture its output for use below.
         archive_lines = tuple(
         archive_lines = tuple(
             execute_command(
             execute_command(
-                make_list_command(
-                    repository, storage_config, repository_arguments, local_path, remote_path
+                rlist.make_rlist_command(
+                    repository,
+                    storage_config,
+                    local_borg_version,
+                    rlist_arguments,
+                    local_path,
+                    remote_path,
                 ),
                 ),
                 output_log_level=None,
                 output_log_level=None,
                 borg_local_path=local_path,
                 borg_local_path=local_path,
@@ -144,19 +159,18 @@ def list_archives(repository, storage_config, list_arguments, local_path='borg',
         archive_lines = (list_arguments.archive,)
         archive_lines = (list_arguments.archive,)
 
 
     # For each archive listed by Borg, run list on the contents of that archive.
     # For each archive listed by Borg, run list on the contents of that archive.
-    for archive_line in archive_lines:
-        try:
-            archive = archive_line.split()[0]
-        except (AttributeError, IndexError):
-            archive = None
-
-        if archive:
-            logger.warning(archive_line)
+    for archive in archive_lines:
+        logger.warning(f'{repository}: Listing archive {archive}')
 
 
         archive_arguments = copy.copy(list_arguments)
         archive_arguments = copy.copy(list_arguments)
         archive_arguments.archive = archive
         archive_arguments.archive = archive
         main_command = make_list_command(
         main_command = make_list_command(
-            repository, storage_config, archive_arguments, local_path, remote_path
+            repository,
+            storage_config,
+            local_borg_version,
+            archive_arguments,
+            local_path,
+            remote_path,
         ) + make_find_paths(list_arguments.find_paths)
         ) + make_find_paths(list_arguments.find_paths)
 
 
         output = execute_command(
         output = execute_command(

+ 121 - 0
borgmatic/borg/rlist.py

@@ -0,0 +1,121 @@
+import logging
+
+from borgmatic.borg import environment, feature, flags
+from borgmatic.execute import execute_command
+
+logger = logging.getLogger(__name__)
+
+
+def resolve_archive_name(
+    repository, archive, storage_config, local_borg_version, local_path='borg', remote_path=None
+):
+    '''
+    Given a local or remote repository path, an archive name, a storage config dict, a local Borg
+    path, and a remote Borg path, simply return the archive name. But if the archive name is
+    "latest", then instead introspect the repository for the latest archive and return its name.
+
+    Raise ValueError if "latest" is given but there are no archives in the repository.
+    '''
+    if archive != "latest":
+        return archive
+
+    lock_wait = storage_config.get('lock_wait', None)
+
+    full_command = (
+        (
+            local_path,
+            'rlist' if feature.available(feature.Feature.RLIST, local_borg_version) else 'list',
+        )
+        + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+        + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+        + flags.make_flags('remote-path', remote_path)
+        + flags.make_flags('lock-wait', lock_wait)
+        + flags.make_flags('last', 1)
+        + ('--short',)
+        + flags.make_repository_flags(repository, local_borg_version)
+    )
+
+    output = execute_command(
+        full_command,
+        output_log_level=None,
+        borg_local_path=local_path,
+        extra_environment=environment.make_environment(storage_config),
+    )
+    try:
+        latest_archive = output.strip().splitlines()[-1]
+    except IndexError:
+        raise ValueError('No archives found in the repository')
+
+    logger.debug('{}: Latest archive is {}'.format(repository, latest_archive))
+
+    return latest_archive
+
+
+MAKE_FLAGS_EXCLUDES = ('repository',)
+
+
+def make_rlist_command(
+    repository,
+    storage_config,
+    local_borg_version,
+    rlist_arguments,
+    local_path='borg',
+    remote_path=None,
+):
+    '''
+    Given a local or remote repository path, a storage config dict, the local Borg version, the
+    arguments to the rlist action, and local and remote Borg paths, return a command as a tuple to
+    list archives with a repository.
+    '''
+    lock_wait = storage_config.get('lock_wait', None)
+
+    return (
+        (
+            local_path,
+            'rlist' if feature.available(feature.Feature.RLIST, local_borg_version) else 'list',
+        )
+        + (
+            ('--info',)
+            if logger.getEffectiveLevel() == logging.INFO and not rlist_arguments.json
+            else ()
+        )
+        + (
+            ('--debug', '--show-rc')
+            if logger.isEnabledFor(logging.DEBUG) and not rlist_arguments.json
+            else ()
+        )
+        + flags.make_flags('remote-path', remote_path)
+        + flags.make_flags('lock-wait', lock_wait)
+        + flags.make_flags_from_arguments(rlist_arguments, excludes=MAKE_FLAGS_EXCLUDES,)
+        + flags.make_repository_flags(repository, local_borg_version)
+    )
+
+
+def list_repository(
+    repository,
+    storage_config,
+    local_borg_version,
+    rlist_arguments,
+    local_path='borg',
+    remote_path=None,
+):
+    '''
+    Given a local or remote repository path, a storage config dict, the local Borg version, the
+    arguments to the list action, and local and remote Borg paths, display the output of listing
+    Borg archives in the given repository (or return JSON output).
+    '''
+    borg_environment = environment.make_environment(storage_config)
+
+    main_command = make_rlist_command(
+        repository, storage_config, local_borg_version, rlist_arguments, local_path, remote_path
+    )
+
+    output = execute_command(
+        main_command,
+        output_log_level=None if rlist_arguments.json else logging.WARNING,
+        borg_local_path=local_path,
+        extra_environment=borg_environment,
+    )
+
+    if rlist_arguments.json:
+        return output

+ 43 - 6
borgmatic/commands/arguments.py

@@ -14,6 +14,7 @@ SUBPARSER_ALIASES = {
     'mount': ['--mount', '-m'],
     'mount': ['--mount', '-m'],
     'umount': ['--umount', '-u'],
     'umount': ['--umount', '-u'],
     'restore': ['--restore', '-r'],
     'restore': ['--restore', '-r'],
+    'rlist': [],
     'list': ['--list', '-l'],
     'list': ['--list', '-l'],
     'rinfo': [],
     'rinfo': [],
     'info': ['--info', '-i'],
     'info': ['--info', '-i'],
@@ -546,18 +547,54 @@ def make_parsers():
         '-h', '--help', action='help', help='Show this help message and exit'
         '-h', '--help', action='help', help='Show this help message and exit'
     )
     )
 
 
+    rlist_parser = subparsers.add_parser(
+        'rlist',
+        aliases=SUBPARSER_ALIASES['rlist'],
+        help='List repository',
+        description='List the archives in a repository',
+        add_help=False,
+    )
+    rlist_group = rlist_parser.add_argument_group('rlist arguments')
+    rlist_group.add_argument(
+        '--repository', help='Path of repository to list, defaults to the configured repositories',
+    )
+    rlist_group.add_argument(
+        '--short', default=False, action='store_true', help='Output only archive names'
+    )
+    rlist_group.add_argument('--format', help='Format for archive listing')
+    rlist_group.add_argument(
+        '--json', default=False, action='store_true', help='Output results as JSON'
+    )
+    rlist_group.add_argument(
+        '-P', '--prefix', help='Only list archive names starting with this prefix'
+    )
+    rlist_group.add_argument(
+        '-a', '--glob-archives', metavar='GLOB', help='Only list archive names matching this glob'
+    )
+    rlist_group.add_argument(
+        '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
+    )
+    rlist_group.add_argument(
+        '--first', metavar='N', help='List first N archives after other filters are applied'
+    )
+    rlist_group.add_argument(
+        '--last', metavar='N', help='List last N archives after other filters are applied'
+    )
+    rlist_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
+
     list_parser = subparsers.add_parser(
     list_parser = subparsers.add_parser(
         'list',
         'list',
         aliases=SUBPARSER_ALIASES['list'],
         aliases=SUBPARSER_ALIASES['list'],
-        help='List archives',
-        description='List archives or the contents of an archive',
+        help='List archive',
+        description='List the files in an archive or search for a file across archives',
         add_help=False,
         add_help=False,
     )
     )
     list_group = list_parser.add_argument_group('list arguments')
     list_group = list_parser.add_argument_group('list arguments')
     list_group.add_argument(
     list_group.add_argument(
-        '--repository', help='Path of repository to list, defaults to the configured repositories',
+        '--repository',
+        help='Path of repository containing archive to list, defaults to the configured repositories',
     )
     )
-    list_group.add_argument('--archive', help='Name of archive to list (or "latest")')
+    list_group.add_argument('--archive', help='Name of the archive to list (or "latest")')
     list_group.add_argument(
     list_group.add_argument(
         '--path',
         '--path',
         metavar='PATH',
         metavar='PATH',
@@ -573,7 +610,7 @@ def make_parsers():
         help='Partial paths or patterns to search for and list across multiple archives',
         help='Partial paths or patterns to search for and list across multiple archives',
     )
     )
     list_group.add_argument(
     list_group.add_argument(
-        '--short', default=False, action='store_true', help='Output only archive or path names'
+        '--short', default=False, action='store_true', help='Output only path names'
     )
     )
     list_group.add_argument('--format', help='Format for file listing')
     list_group.add_argument('--format', help='Format for file listing')
     list_group.add_argument(
     list_group.add_argument(
@@ -589,7 +626,7 @@ def make_parsers():
         '--successful',
         '--successful',
         default=True,
         default=True,
         action='store_true',
         action='store_true',
-        help='Deprecated in favor of listing successful (non-checkpoint) backups by default in newer versions of Borg',
+        help='Deprecated; no effect. Newer versions of Borg list successful (non-checkpoint) archives by default.',
     )
     )
     list_group.add_argument(
     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'

+ 73 - 17
borgmatic/commands/borgmatic.py

@@ -25,6 +25,7 @@ from borgmatic.borg import mount as borg_mount
 from borgmatic.borg import prune as borg_prune
 from borgmatic.borg import prune as borg_prune
 from borgmatic.borg import rcreate as borg_rcreate
 from borgmatic.borg import rcreate as borg_rcreate
 from borgmatic.borg import rinfo as borg_rinfo
 from borgmatic.borg import rinfo as borg_rinfo
+from borgmatic.borg import rlist as borg_rlist
 from borgmatic.borg import umount as borg_umount
 from borgmatic.borg import umount as borg_umount
 from borgmatic.borg import version as borg_version
 from borgmatic.borg import version as borg_version
 from borgmatic.commands.arguments import parse_arguments
 from borgmatic.commands.arguments import parse_arguments
@@ -434,8 +435,13 @@ def run_actions(
             borg_extract.extract_archive(
             borg_extract.extract_archive(
                 global_arguments.dry_run,
                 global_arguments.dry_run,
                 repository,
                 repository,
-                borg_list.resolve_archive_name(
-                    repository, arguments['extract'].archive, storage, local_path, remote_path
+                borg_rlist.resolve_archive_name(
+                    repository,
+                    arguments['extract'].archive,
+                    storage,
+                    local_borg_version,
+                    local_path,
+                    remote_path,
                 ),
                 ),
                 arguments['extract'].paths,
                 arguments['extract'].paths,
                 location,
                 location,
@@ -467,8 +473,13 @@ def run_actions(
             borg_export_tar.export_tar_archive(
             borg_export_tar.export_tar_archive(
                 global_arguments.dry_run,
                 global_arguments.dry_run,
                 repository,
                 repository,
-                borg_list.resolve_archive_name(
-                    repository, arguments['export-tar'].archive, storage, local_path, remote_path
+                borg_rlist.resolve_archive_name(
+                    repository,
+                    arguments['export-tar'].archive,
+                    storage,
+                    local_borg_version,
+                    local_path,
+                    remote_path,
                 ),
                 ),
                 arguments['export-tar'].paths,
                 arguments['export-tar'].paths,
                 arguments['export-tar'].destination,
                 arguments['export-tar'].destination,
@@ -492,8 +503,13 @@ def run_actions(
 
 
             borg_mount.mount_archive(
             borg_mount.mount_archive(
                 repository,
                 repository,
-                borg_list.resolve_archive_name(
-                    repository, arguments['mount'].archive, storage, local_path, remote_path
+                borg_rlist.resolve_archive_name(
+                    repository,
+                    arguments['mount'].archive,
+                    storage,
+                    local_borg_version,
+                    local_path,
+                    remote_path,
                 ),
                 ),
                 arguments['mount'].mount_point,
                 arguments['mount'].mount_point,
                 arguments['mount'].paths,
                 arguments['mount'].paths,
@@ -525,8 +541,13 @@ def run_actions(
             if 'all' in restore_names:
             if 'all' in restore_names:
                 restore_names = []
                 restore_names = []
 
 
-            archive_name = borg_list.resolve_archive_name(
-                repository, arguments['restore'].archive, storage, local_path, remote_path
+            archive_name = borg_rlist.resolve_archive_name(
+                repository,
+                arguments['restore'].archive,
+                storage,
+                local_borg_version,
+                local_path,
+                remote_path,
             )
             )
             found_names = set()
             found_names = set()
 
 
@@ -596,20 +617,45 @@ def run_actions(
                         ', '.join(missing_names)
                         ', '.join(missing_names)
                     )
                     )
                 )
                 )
-
+    if 'rlist' in arguments:
+        if arguments['rlist'].repository is None or validate.repositories_match(
+            repository, arguments['rlist'].repository
+        ):
+            rlist_arguments = copy.copy(arguments['rlist'])
+            if not rlist_arguments.json:  # pragma: nocover
+                logger.warning('{}: Listing repository'.format(repository))
+            json_output = borg_rlist.list_repository(
+                repository,
+                storage,
+                local_borg_version,
+                rlist_arguments=rlist_arguments,
+                local_path=local_path,
+                remote_path=remote_path,
+            )
+            if json_output:  # pragma: nocover
+                yield json.loads(json_output)
     if 'list' in arguments:
     if 'list' in arguments:
         if arguments['list'].repository is None or validate.repositories_match(
         if arguments['list'].repository is None or validate.repositories_match(
             repository, arguments['list'].repository
             repository, arguments['list'].repository
         ):
         ):
             list_arguments = copy.copy(arguments['list'])
             list_arguments = copy.copy(arguments['list'])
             if not list_arguments.json:  # pragma: nocover
             if not list_arguments.json:  # pragma: nocover
-                logger.warning('{}: Listing archives'.format(repository))
-            list_arguments.archive = borg_list.resolve_archive_name(
-                repository, list_arguments.archive, storage, local_path, remote_path
+                if list_arguments.find_paths:
+                    logger.warning('{}: Searching archives'.format(repository))
+                else:
+                    logger.warning('{}: Listing archive'.format(repository))
+            list_arguments.archive = borg_rlist.resolve_archive_name(
+                repository,
+                list_arguments.archive,
+                storage,
+                local_borg_version,
+                local_path,
+                remote_path,
             )
             )
-            json_output = borg_list.list_archives(
+            json_output = borg_list.list_archive(
                 repository,
                 repository,
                 storage,
                 storage,
+                local_borg_version,
                 list_arguments=list_arguments,
                 list_arguments=list_arguments,
                 local_path=local_path,
                 local_path=local_path,
                 remote_path=remote_path,
                 remote_path=remote_path,
@@ -640,8 +686,13 @@ def run_actions(
             info_arguments = copy.copy(arguments['info'])
             info_arguments = copy.copy(arguments['info'])
             if not info_arguments.json:  # pragma: nocover
             if not info_arguments.json:  # pragma: nocover
                 logger.warning('{}: Displaying archive summary information'.format(repository))
                 logger.warning('{}: Displaying archive summary information'.format(repository))
-            info_arguments.archive = borg_list.resolve_archive_name(
-                repository, info_arguments.archive, storage, local_path, remote_path
+            info_arguments.archive = borg_rlist.resolve_archive_name(
+                repository,
+                info_arguments.archive,
+                storage,
+                local_borg_version,
+                local_path,
+                remote_path,
             )
             )
             json_output = borg_info.display_archives_info(
             json_output = borg_info.display_archives_info(
                 repository,
                 repository,
@@ -658,8 +709,13 @@ def run_actions(
             repository, arguments['borg'].repository
             repository, arguments['borg'].repository
         ):
         ):
             logger.warning('{}: Running arbitrary Borg command'.format(repository))
             logger.warning('{}: Running arbitrary Borg command'.format(repository))
-            archive_name = borg_list.resolve_archive_name(
-                repository, arguments['borg'].archive, storage, local_path, remote_path
+            archive_name = borg_rlist.resolve_archive_name(
+                repository,
+                arguments['borg'].archive,
+                storage,
+                local_borg_version,
+                local_path,
+                remote_path,
             )
             )
             borg_borg.run_arbitrary_borg(
             borg_borg.run_arbitrary_borg(
                 repository,
                 repository,

+ 1 - 1
docs/Dockerfile

@@ -4,7 +4,7 @@ COPY . /app
 RUN apk add --no-cache py3-pip py3-ruamel.yaml py3-ruamel.yaml.clib
 RUN apk add --no-cache py3-pip py3-ruamel.yaml py3-ruamel.yaml.clib
 RUN pip install --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml
 RUN pip install --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml
 RUN borgmatic --help > /command-line.txt \
 RUN borgmatic --help > /command-line.txt \
-    && for action in init prune compact create check extract export-tar mount umount restore list info borg; do \
+    && for action in rcreate prune compact create check extract export-tar mount umount restore rlist list rinfo info borg; do \
            echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
            echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
            && borgmatic "$action" --help >> /command-line.txt; done
            && borgmatic "$action" --help >> /command-line.txt; done
 
 

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

@@ -133,14 +133,13 @@ that you'd like supported.
 
 
 To restore a database dump from an archive, use the `borgmatic restore`
 To restore a database dump from an archive, use the `borgmatic restore`
 action. But the first step is to figure out which archive to restore from. A
 action. But the first step is to figure out which archive to restore from. A
-good way to do that is to use the `list` action:
+good way to do that is to use the `rlist` action:
 
 
 ```bash
 ```bash
-borgmatic list
+borgmatic rlist
 ```
 ```
 
 
-(No borgmatic `list` action? Try the old-style `--list`, or upgrade
-borgmatic!)
+(No borgmatic `rlist` action? Try `list` instead or upgrade borgmatic!)
 
 
 That should yield output looking something like:
 That should yield output looking something like:
 
 

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

@@ -9,14 +9,13 @@ eleventyNavigation:
 
 
 When the worst happens—or you want to test your backups—the first step is
 When the worst happens—or you want to test your backups—the first step is
 to figure out which archive to extract. A good way to do that is to use the
 to figure out which archive to extract. A good way to do that is to use the
-`list` action:
+`rlist` action:
 
 
 ```bash
 ```bash
-borgmatic list
+borgmatic rlist
 ```
 ```
 
 
-(No borgmatic `list` action? Try the old-style `--list`, or upgrade
-borgmatic!)
+(No borgmatic `rlist` action? Try `list` instead or upgrade borgmatic!)
 
 
 That should yield output looking something like:
 That should yield output looking something like:
 
 

+ 9 - 3
docs/how-to/inspect-your-backups.md

@@ -46,14 +46,20 @@ borgmatic list
 borgmatic info
 borgmatic info
 ```
 ```
 
 
-<span class="minilink minilink-addedin">New in borgmatic version 2.0.0</span>
-There's also an `rinfo` action for displaying repository information with Borg
-2.x:
+<span class="minilink minilink-addedin">New in borgmatic version 1.7.0</span>
+There are also `rlist` and `rinfo` actions for displaying repository
+information with Borg 2.x:
 
 
 ```bash
 ```bash
+borgmatic rlist
 borgmatic rinfo
 borgmatic rinfo
 ```
 ```
 
 
+See the [borgmatic command-line
+reference](https://torsion.org/borgmatic/docs/reference/command-line/) for
+more information.
+
+
 ### Searching for a file
 ### Searching for a file
 
 
 <span class="minilink minilink-addedin">New in version 1.6.3</span> Let's say
 <span class="minilink minilink-addedin">New in version 1.6.3</span> Let's say

+ 3 - 3
docs/how-to/monitor-your-backups.md

@@ -329,9 +329,9 @@ output only shows up at the console, and not in syslog.
 
 
 ### Latest backups
 ### Latest backups
 
 
-All borgmatic actions that accept an "--archive" flag allow you to specify an
-archive name of "latest". This lets you get the latest archive without having
-to first run "borgmatic list" manually, which can be handy in automated
+All borgmatic actions that accept an `--archive` flag allow you to specify an
+archive name of `latest`. This lets you get the latest archive without having
+to first run `borgmatic rlist` manually, which can be handy in automated
 scripts. Here's an example:
 scripts. Here's an example:
 
 
 ```bash
 ```bash

+ 4 - 5
docs/how-to/run-arbitrary-borg-commands.md

@@ -46,12 +46,11 @@ options, as that part is provided by borgmatic.
 You can also specify Borg options for relevant commands:
 You can also specify Borg options for relevant commands:
 
 
 ```bash
 ```bash
-borgmatic borg list --progress
+borgmatic borg rlist --short
 ```
 ```
 
 
-This runs Borg's `list` command once on each configured borgmatic
-repository. However, the native `borgmatic list` action should be preferred
-for most use.
+This runs Borg's `rlist` command once on each configured borgmatic repository.
+However, the native `borgmatic rlist` action should be preferred for most use.
 
 
 What if you only want to run Borg on a single configured borgmatic repository
 What if you only want to run Borg on a single configured borgmatic repository
 when you've got several configured? Not a problem.
 when you've got several configured? Not a problem.
@@ -63,7 +62,7 @@ borgmatic borg --repository repo.borg break-lock
 And what about a single archive?
 And what about a single archive?
 
 
 ```bash
 ```bash
-borgmatic borg --archive your-archive-name list
+borgmatic borg --archive your-archive-name rlist
 ```
 ```
 
 
 ### Limitations
 ### Limitations

+ 44 - 45
tests/unit/borg/test_extract.py

@@ -23,101 +23,100 @@ def insert_execute_command_output_mock(command, result):
 
 
 
 
 def test_extract_last_archive_dry_run_calls_borg_with_last_archive():
 def test_extract_last_archive_dry_run_calls_borg_with_last_archive():
-    insert_execute_command_output_mock(
-        ('borg', 'list', '--short', 'repo'), result='archive1\narchive2\n'
-    )
-    insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive2'))
-    flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive')
+    insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive'))
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
-        ('repo::archive2',)
+        ('repo::archive',)
     )
     )
 
 
-    module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None)
+    module.extract_last_archive_dry_run(
+        storage_config={}, local_borg_version='1.2.3', repository='repo', lock_wait=None
+    )
 
 
 
 
 def test_extract_last_archive_dry_run_without_any_archives_should_not_raise():
 def test_extract_last_archive_dry_run_without_any_archives_should_not_raise():
-    insert_execute_command_output_mock(('borg', 'list', '--short', 'repo'), result='\n')
-    flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.rlist).should_receive('resolve_archive_name').and_raise(ValueError)
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(('repo',))
 
 
-    module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None)
+    module.extract_last_archive_dry_run(
+        storage_config={}, local_borg_version='1.2.3', repository='repo', lock_wait=None
+    )
 
 
 
 
 def test_extract_last_archive_dry_run_with_log_info_calls_borg_with_info_parameter():
 def test_extract_last_archive_dry_run_with_log_info_calls_borg_with_info_parameter():
-    insert_execute_command_output_mock(
-        ('borg', 'list', '--short', '--info', 'repo'), result='archive1\narchive2\n'
-    )
-    insert_execute_command_mock(('borg', 'extract', '--dry-run', '--info', 'repo::archive2'))
+    flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive')
+    insert_execute_command_mock(('borg', 'extract', '--dry-run', '--info', 'repo::archive'))
     insert_logging_mock(logging.INFO)
     insert_logging_mock(logging.INFO)
-    flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
-        ('repo::archive2',)
+        ('repo::archive',)
     )
     )
 
 
-    module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None)
+    module.extract_last_archive_dry_run(
+        storage_config={}, local_borg_version='1.2.3', repository='repo', lock_wait=None
+    )
 
 
 
 
 def test_extract_last_archive_dry_run_with_log_debug_calls_borg_with_debug_parameter():
 def test_extract_last_archive_dry_run_with_log_debug_calls_borg_with_debug_parameter():
-    insert_execute_command_output_mock(
-        ('borg', 'list', '--short', '--debug', '--show-rc', 'repo'), result='archive1\narchive2\n'
-    )
+    flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive')
     insert_execute_command_mock(
     insert_execute_command_mock(
-        ('borg', 'extract', '--dry-run', '--debug', '--show-rc', '--list', 'repo::archive2')
+        ('borg', 'extract', '--dry-run', '--debug', '--show-rc', '--list', 'repo::archive')
     )
     )
     insert_logging_mock(logging.DEBUG)
     insert_logging_mock(logging.DEBUG)
-    flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
-        ('repo::archive2',)
+        ('repo::archive',)
     )
     )
 
 
-    module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None)
+    module.extract_last_archive_dry_run(
+        storage_config={}, local_borg_version='1.2.3', repository='repo', lock_wait=None
+    )
 
 
 
 
 def test_extract_last_archive_dry_run_calls_borg_via_local_path():
 def test_extract_last_archive_dry_run_calls_borg_via_local_path():
-    insert_execute_command_output_mock(
-        ('borg1', 'list', '--short', 'repo'), result='archive1\narchive2\n'
-    )
-    insert_execute_command_mock(('borg1', 'extract', '--dry-run', 'repo::archive2'))
-    flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive')
+    insert_execute_command_mock(('borg1', 'extract', '--dry-run', 'repo::archive'))
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
-        ('repo::archive2',)
+        ('repo::archive',)
     )
     )
 
 
     module.extract_last_archive_dry_run(
     module.extract_last_archive_dry_run(
-        storage_config={}, repository='repo', lock_wait=None, local_path='borg1'
+        storage_config={},
+        local_borg_version='1.2.3',
+        repository='repo',
+        lock_wait=None,
+        local_path='borg1',
     )
     )
 
 
 
 
 def test_extract_last_archive_dry_run_calls_borg_with_remote_path_parameters():
 def test_extract_last_archive_dry_run_calls_borg_with_remote_path_parameters():
-    insert_execute_command_output_mock(
-        ('borg', 'list', '--short', '--remote-path', 'borg1', 'repo'), result='archive1\narchive2\n'
-    )
+    flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive')
     insert_execute_command_mock(
     insert_execute_command_mock(
-        ('borg', 'extract', '--dry-run', '--remote-path', 'borg1', 'repo::archive2')
+        ('borg', 'extract', '--dry-run', '--remote-path', 'borg1', 'repo::archive')
     )
     )
-    flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
-        ('repo::archive2',)
+        ('repo::archive',)
     )
     )
 
 
     module.extract_last_archive_dry_run(
     module.extract_last_archive_dry_run(
-        storage_config={}, repository='repo', lock_wait=None, remote_path='borg1'
+        storage_config={},
+        local_borg_version='1.2.3',
+        repository='repo',
+        lock_wait=None,
+        remote_path='borg1',
     )
     )
 
 
 
 
 def test_extract_last_archive_dry_run_calls_borg_with_lock_wait_parameters():
 def test_extract_last_archive_dry_run_calls_borg_with_lock_wait_parameters():
-    insert_execute_command_output_mock(
-        ('borg', 'list', '--short', '--lock-wait', '5', 'repo'), result='archive1\narchive2\n'
-    )
+    flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive')
     insert_execute_command_mock(
     insert_execute_command_mock(
-        ('borg', 'extract', '--dry-run', '--lock-wait', '5', 'repo::archive2')
+        ('borg', 'extract', '--dry-run', '--lock-wait', '5', 'repo::archive')
     )
     )
-    flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
-        ('repo::archive2',)
+        ('repo::archive',)
     )
     )
 
 
-    module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=5)
+    module.extract_last_archive_dry_run(
+        storage_config={}, local_borg_version='1.2.3', repository='repo', lock_wait=5
+    )
 
 
 
 
 def test_extract_archive_calls_borg_with_path_parameters():
 def test_extract_archive_calls_borg_with_path_parameters():

+ 244 - 147
tests/unit/borg/test_list.py

@@ -8,129 +8,17 @@ from borgmatic.borg import list as module
 
 
 from ..test_verbosity import insert_logging_mock
 from ..test_verbosity import insert_logging_mock
 
 
-BORG_LIST_LATEST_ARGUMENTS = (
-    '--last',
-    '1',
-    '--short',
-    'repo',
-)
-
-
-def test_resolve_archive_name_passes_through_non_latest_archive_name():
-    archive = 'myhost-2030-01-01T14:41:17.647620'
-
-    assert module.resolve_archive_name('repo', archive, storage_config={}) == archive
-
-
-def test_resolve_archive_name_calls_borg_with_parameters():
-    expected_archive = 'archive-name'
-    flexmock(module.environment).should_receive('make_environment')
-    flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
-        output_log_level=None,
-        borg_local_path='borg',
-        extra_environment=None,
-    ).and_return(expected_archive + '\n')
-
-    assert module.resolve_archive_name('repo', 'latest', storage_config={}) == expected_archive
-
-
-def test_resolve_archive_name_with_log_info_calls_borg_with_info_parameter():
-    expected_archive = 'archive-name'
-    flexmock(module.environment).should_receive('make_environment')
-    flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'list', '--info') + BORG_LIST_LATEST_ARGUMENTS,
-        output_log_level=None,
-        borg_local_path='borg',
-        extra_environment=None,
-    ).and_return(expected_archive + '\n')
-    insert_logging_mock(logging.INFO)
-
-    assert module.resolve_archive_name('repo', 'latest', storage_config={}) == expected_archive
-
-
-def test_resolve_archive_name_with_log_debug_calls_borg_with_debug_parameter():
-    expected_archive = 'archive-name'
-    flexmock(module.environment).should_receive('make_environment')
-    flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'list', '--debug', '--show-rc') + BORG_LIST_LATEST_ARGUMENTS,
-        output_log_level=None,
-        borg_local_path='borg',
-        extra_environment=None,
-    ).and_return(expected_archive + '\n')
-    insert_logging_mock(logging.DEBUG)
-
-    assert module.resolve_archive_name('repo', 'latest', storage_config={}) == expected_archive
-
-
-def test_resolve_archive_name_with_local_path_calls_borg_via_local_path():
-    expected_archive = 'archive-name'
-    flexmock(module.environment).should_receive('make_environment')
-    flexmock(module).should_receive('execute_command').with_args(
-        ('borg1', 'list') + BORG_LIST_LATEST_ARGUMENTS,
-        output_log_level=None,
-        borg_local_path='borg1',
-        extra_environment=None,
-    ).and_return(expected_archive + '\n')
-
-    assert (
-        module.resolve_archive_name('repo', 'latest', storage_config={}, local_path='borg1')
-        == expected_archive
-    )
-
-
-def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_parameters():
-    expected_archive = 'archive-name'
-    flexmock(module.environment).should_receive('make_environment')
-    flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'list', '--remote-path', 'borg1') + BORG_LIST_LATEST_ARGUMENTS,
-        output_log_level=None,
-        borg_local_path='borg',
-        extra_environment=None,
-    ).and_return(expected_archive + '\n')
-
-    assert (
-        module.resolve_archive_name('repo', 'latest', storage_config={}, remote_path='borg1')
-        == expected_archive
-    )
-
-
-def test_resolve_archive_name_without_archives_raises():
-    flexmock(module.environment).should_receive('make_environment')
-    flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
-        output_log_level=None,
-        borg_local_path='borg',
-        extra_environment=None,
-    ).and_return('')
-
-    with pytest.raises(ValueError):
-        module.resolve_archive_name('repo', 'latest', storage_config={})
-
-
-def test_resolve_archive_name_with_lock_wait_calls_borg_with_lock_wait_parameters():
-    expected_archive = 'archive-name'
-
-    flexmock(module.environment).should_receive('make_environment')
-    flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'list', '--lock-wait', 'okay') + BORG_LIST_LATEST_ARGUMENTS,
-        output_log_level=None,
-        borg_local_path='borg',
-        extra_environment=None,
-    ).and_return(expected_archive + '\n')
-
-    assert (
-        module.resolve_archive_name('repo', 'latest', storage_config={'lock_wait': 'okay'})
-        == expected_archive
-    )
-
 
 
 def test_make_list_command_includes_log_info():
 def test_make_list_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_from_arguments').and_return(())
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
 
 
     command = module.make_list_command(
     command = module.make_list_command(
         repository='repo',
         repository='repo',
         storage_config={},
         storage_config={},
+        local_borg_version='1.2.3',
         list_arguments=flexmock(archive=None, paths=None, json=False),
         list_arguments=flexmock(archive=None, paths=None, json=False),
     )
     )
 
 
@@ -139,10 +27,14 @@ def test_make_list_command_includes_log_info():
 
 
 def test_make_list_command_includes_json_but_not_info():
 def test_make_list_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_from_arguments').and_return(('--json',))
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
 
 
     command = module.make_list_command(
     command = module.make_list_command(
         repository='repo',
         repository='repo',
         storage_config={},
         storage_config={},
+        local_borg_version='1.2.3',
         list_arguments=flexmock(archive=None, paths=None, json=True),
         list_arguments=flexmock(archive=None, paths=None, json=True),
     )
     )
 
 
@@ -151,10 +43,14 @@ def test_make_list_command_includes_json_but_not_info():
 
 
 def test_make_list_command_includes_log_debug():
 def test_make_list_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_from_arguments').and_return(())
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
 
 
     command = module.make_list_command(
     command = module.make_list_command(
         repository='repo',
         repository='repo',
         storage_config={},
         storage_config={},
+        local_borg_version='1.2.3',
         list_arguments=flexmock(archive=None, paths=None, json=False),
         list_arguments=flexmock(archive=None, paths=None, json=False),
     )
     )
 
 
@@ -163,10 +59,14 @@ def test_make_list_command_includes_log_debug():
 
 
 def test_make_list_command_includes_json_but_not_debug():
 def test_make_list_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_from_arguments').and_return(('--json',))
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
 
 
     command = module.make_list_command(
     command = module.make_list_command(
         repository='repo',
         repository='repo',
         storage_config={},
         storage_config={},
+        local_borg_version='1.2.3',
         list_arguments=flexmock(archive=None, paths=None, json=True),
         list_arguments=flexmock(archive=None, paths=None, json=True),
     )
     )
 
 
@@ -174,9 +74,14 @@ def test_make_list_command_includes_json_but_not_debug():
 
 
 
 
 def test_make_list_command_includes_json():
 def test_make_list_command_includes_json():
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+
     command = module.make_list_command(
     command = module.make_list_command(
         repository='repo',
         repository='repo',
         storage_config={},
         storage_config={},
+        local_borg_version='1.2.3',
         list_arguments=flexmock(archive=None, paths=None, json=True),
         list_arguments=flexmock(archive=None, paths=None, json=True),
     )
     )
 
 
@@ -184,9 +89,16 @@ def test_make_list_command_includes_json():
 
 
 
 
 def test_make_list_command_includes_lock_wait():
 def test_make_list_command_includes_lock_wait():
+    flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(
+        ('--lock-wait', '5')
+    )
+    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_list_command(
     command = module.make_list_command(
         repository='repo',
         repository='repo',
         storage_config={'lock_wait': 5},
         storage_config={'lock_wait': 5},
+        local_borg_version='1.2.3',
         list_arguments=flexmock(archive=None, paths=None, json=False),
         list_arguments=flexmock(archive=None, paths=None, json=False),
     )
     )
 
 
@@ -194,9 +106,16 @@ def test_make_list_command_includes_lock_wait():
 
 
 
 
 def test_make_list_command_includes_archive():
 def test_make_list_command_includes_archive():
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
+    flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
+        ('repo::archive',)
+    )
+
     command = module.make_list_command(
     command = module.make_list_command(
         repository='repo',
         repository='repo',
         storage_config={},
         storage_config={},
+        local_borg_version='1.2.3',
         list_arguments=flexmock(archive='archive', paths=None, json=False),
         list_arguments=flexmock(archive='archive', paths=None, json=False),
     )
     )
 
 
@@ -204,9 +123,16 @@ def test_make_list_command_includes_archive():
 
 
 
 
 def test_make_list_command_includes_archive_and_path():
 def test_make_list_command_includes_archive_and_path():
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
+    flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
+        ('repo::archive',)
+    )
+
     command = module.make_list_command(
     command = module.make_list_command(
         repository='repo',
         repository='repo',
         storage_config={},
         storage_config={},
+        local_borg_version='1.2.3',
         list_arguments=flexmock(archive='archive', paths=['var/lib'], json=False),
         list_arguments=flexmock(archive='archive', paths=['var/lib'], json=False),
     )
     )
 
 
@@ -214,9 +140,14 @@ def test_make_list_command_includes_archive_and_path():
 
 
 
 
 def test_make_list_command_includes_local_path():
 def test_make_list_command_includes_local_path():
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    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_list_command(
     command = module.make_list_command(
         repository='repo',
         repository='repo',
         storage_config={},
         storage_config={},
+        local_borg_version='1.2.3',
         list_arguments=flexmock(archive=None, paths=None, json=False),
         list_arguments=flexmock(archive=None, paths=None, json=False),
         local_path='borg2',
         local_path='borg2',
     )
     )
@@ -225,9 +156,16 @@ def test_make_list_command_includes_local_path():
 
 
 
 
 def test_make_list_command_includes_remote_path():
 def test_make_list_command_includes_remote_path():
+    flexmock(module.flags).should_receive('make_flags').and_return(
+        ('--remote-path', 'borg2')
+    ).and_return(())
+    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_list_command(
     command = module.make_list_command(
         repository='repo',
         repository='repo',
         storage_config={},
         storage_config={},
+        local_borg_version='1.2.3',
         list_arguments=flexmock(archive=None, paths=None, json=False),
         list_arguments=flexmock(archive=None, paths=None, json=False),
         remote_path='borg2',
         remote_path='borg2',
     )
     )
@@ -236,9 +174,14 @@ def test_make_list_command_includes_remote_path():
 
 
 
 
 def test_make_list_command_includes_short():
 def test_make_list_command_includes_short():
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--short',))
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+
     command = module.make_list_command(
     command = module.make_list_command(
         repository='repo',
         repository='repo',
         storage_config={},
         storage_config={},
+        local_borg_version='1.2.3',
         list_arguments=flexmock(archive=None, paths=None, json=False, short=True),
         list_arguments=flexmock(archive=None, paths=None, json=False, short=True),
     )
     )
 
 
@@ -260,16 +203,23 @@ def test_make_list_command_includes_short():
     ),
     ),
 )
 )
 def test_make_list_command_includes_additional_flags(argument_name):
 def test_make_list_command_includes_additional_flags(argument_name):
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(
+        (f"--{argument_name.replace('_', '-')}", 'value')
+    )
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+
     command = module.make_list_command(
     command = module.make_list_command(
         repository='repo',
         repository='repo',
         storage_config={},
         storage_config={},
+        local_borg_version='1.2.3',
         list_arguments=flexmock(
         list_arguments=flexmock(
             archive=None,
             archive=None,
             paths=None,
             paths=None,
             json=False,
             json=False,
             find_paths=None,
             find_paths=None,
             format=None,
             format=None,
-            **{argument_name: 'value'}
+            **{argument_name: 'value'},
         ),
         ),
     )
     )
 
 
@@ -303,89 +253,109 @@ def test_make_find_paths_adds_globs_to_path_fragments():
     assert module.make_find_paths(('foo.txt',)) == ('sh:**/*foo.txt*/**',)
     assert module.make_find_paths(('foo.txt',)) == ('sh:**/*foo.txt*/**',)
 
 
 
 
-def test_list_archives_calls_borg_with_parameters():
-    list_arguments = argparse.Namespace(archive=None, paths=None, json=False, find_paths=None)
+def test_list_archive_calls_borg_with_parameters():
+    list_arguments = argparse.Namespace(archive='archive', paths=None, json=False, find_paths=None)
 
 
+    flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module).should_receive('make_list_command').with_args(
     flexmock(module).should_receive('make_list_command').with_args(
         repository='repo',
         repository='repo',
         storage_config={},
         storage_config={},
+        local_borg_version='1.2.3',
         list_arguments=list_arguments,
         list_arguments=list_arguments,
         local_path='borg',
         local_path='borg',
         remote_path=None,
         remote_path=None,
-    ).and_return(('borg', 'list', 'repo'))
+    ).and_return(('borg', 'list', 'repo::archive'))
     flexmock(module).should_receive('make_find_paths').and_return(())
     flexmock(module).should_receive('make_find_paths').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'list', 'repo'),
+        ('borg', 'list', 'repo::archive'),
         output_log_level=logging.WARNING,
         output_log_level=logging.WARNING,
         borg_local_path='borg',
         borg_local_path='borg',
         extra_environment=None,
         extra_environment=None,
     ).once()
     ).once()
 
 
-    module.list_archives(
-        repository='repo', storage_config={}, list_arguments=list_arguments,
+    module.list_archive(
+        repository='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        list_arguments=list_arguments,
     )
     )
 
 
 
 
-def test_list_archives_with_json_suppresses_most_borg_output():
-    list_arguments = argparse.Namespace(archive=None, paths=None, json=True, find_paths=None)
+def test_list_archive_with_json_suppresses_most_borg_output():
+    list_arguments = argparse.Namespace(archive='archive', paths=None, json=True, find_paths=None)
 
 
+    flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module).should_receive('make_list_command').with_args(
     flexmock(module).should_receive('make_list_command').with_args(
         repository='repo',
         repository='repo',
         storage_config={},
         storage_config={},
+        local_borg_version='1.2.3',
         list_arguments=list_arguments,
         list_arguments=list_arguments,
         local_path='borg',
         local_path='borg',
         remote_path=None,
         remote_path=None,
-    ).and_return(('borg', 'list', 'repo'))
+    ).and_return(('borg', 'list', 'repo::archive'))
     flexmock(module).should_receive('make_find_paths').and_return(())
     flexmock(module).should_receive('make_find_paths').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'list', 'repo'),
+        ('borg', 'list', 'repo::archive'),
         output_log_level=None,
         output_log_level=None,
         borg_local_path='borg',
         borg_local_path='borg',
         extra_environment=None,
         extra_environment=None,
     ).once()
     ).once()
 
 
-    module.list_archives(
-        repository='repo', storage_config={}, list_arguments=list_arguments,
+    module.list_archive(
+        repository='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        list_arguments=list_arguments,
     )
     )
 
 
 
 
-def test_list_archives_calls_borg_with_local_path():
-    list_arguments = argparse.Namespace(archive=None, paths=None, json=False, find_paths=None)
+def test_list_archive_calls_borg_with_local_path():
+    list_arguments = argparse.Namespace(archive='archive', paths=None, json=False, find_paths=None)
 
 
+    flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module).should_receive('make_list_command').with_args(
     flexmock(module).should_receive('make_list_command').with_args(
         repository='repo',
         repository='repo',
         storage_config={},
         storage_config={},
+        local_borg_version='1.2.3',
         list_arguments=list_arguments,
         list_arguments=list_arguments,
         local_path='borg2',
         local_path='borg2',
         remote_path=None,
         remote_path=None,
-    ).and_return(('borg2', 'list', 'repo'))
+    ).and_return(('borg2', 'list', 'repo::archive'))
     flexmock(module).should_receive('make_find_paths').and_return(())
     flexmock(module).should_receive('make_find_paths').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg2', 'list', 'repo'),
+        ('borg2', 'list', 'repo::archive'),
         output_log_level=logging.WARNING,
         output_log_level=logging.WARNING,
         borg_local_path='borg2',
         borg_local_path='borg2',
         extra_environment=None,
         extra_environment=None,
     ).once()
     ).once()
 
 
-    module.list_archives(
-        repository='repo', storage_config={}, list_arguments=list_arguments, local_path='borg2',
+    module.list_archive(
+        repository='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        list_arguments=list_arguments,
+        local_path='borg2',
     )
     )
 
 
 
 
-def test_list_archives_calls_borg_multiple_times_with_find_paths():
+def test_list_archive_calls_borg_multiple_times_with_find_paths():
     glob_paths = ('**/*foo.txt*/**',)
     glob_paths = ('**/*foo.txt*/**',)
     list_arguments = argparse.Namespace(
     list_arguments = argparse.Namespace(
-        archive=None, paths=None, json=False, find_paths=['foo.txt'], format=None
+        archive=None,
+        json=False,
+        find_paths=['foo.txt'],
+        prefix=None,
+        glob_archives=None,
+        sort_by=None,
+        first=None,
+        last=None,
     )
     )
 
 
-    flexmock(module).should_receive('make_list_command').and_return(
-        ('borg', 'list', 'repo')
-    ).and_return(('borg', 'list', 'repo::archive1')).and_return(('borg', 'list', 'repo::archive2'))
-    flexmock(module).should_receive('make_find_paths').and_return(glob_paths)
-    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.feature).should_receive('available').and_return(False)
+    flexmock(module.rlist).should_receive('make_rlist_command').and_return(('borg', 'list', 'repo'))
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', 'repo'),
         ('borg', 'list', 'repo'),
         output_log_level=None,
         output_log_level=None,
@@ -394,6 +364,10 @@ def test_list_archives_calls_borg_multiple_times_with_find_paths():
     ).and_return(
     ).and_return(
         'archive1   Sun, 2022-05-29 15:27:04 [abc]\narchive2   Mon, 2022-05-30 19:47:15 [xyz]'
         'archive1   Sun, 2022-05-29 15:27:04 [abc]\narchive2   Mon, 2022-05-30 19:47:15 [xyz]'
     ).once()
     ).once()
+    flexmock(module).should_receive('make_list_command').and_return(
+        ('borg', 'list', 'repo::archive1')
+    ).and_return(('borg', 'list', 'repo::archive2'))
+    flexmock(module).should_receive('make_find_paths').and_return(glob_paths)
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', 'repo::archive1') + glob_paths,
         ('borg', 'list', 'repo::archive1') + glob_paths,
@@ -408,17 +382,137 @@ def test_list_archives_calls_borg_multiple_times_with_find_paths():
         extra_environment=None,
         extra_environment=None,
     ).once()
     ).once()
 
 
-    module.list_archives(
-        repository='repo', storage_config={}, list_arguments=list_arguments,
+    module.list_archive(
+        repository='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        list_arguments=list_arguments,
     )
     )
 
 
 
 
-def test_list_archives_calls_borg_with_archive():
+def test_list_archive_calls_borg_with_archive():
     list_arguments = argparse.Namespace(archive='archive', paths=None, json=False, find_paths=None)
     list_arguments = argparse.Namespace(archive='archive', paths=None, json=False, find_paths=None)
 
 
+    flexmock(module.feature).should_receive('available').and_return(False)
+    flexmock(module).should_receive('make_list_command').with_args(
+        repository='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        list_arguments=list_arguments,
+        local_path='borg',
+        remote_path=None,
+    ).and_return(('borg', 'list', 'repo::archive'))
+    flexmock(module).should_receive('make_find_paths').and_return(())
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'list', 'repo::archive'),
+        output_log_level=logging.WARNING,
+        borg_local_path='borg',
+        extra_environment=None,
+    ).once()
+
+    module.list_archive(
+        repository='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        list_arguments=list_arguments,
+    )
+
+
+def test_list_archive_without_archive_delegates_to_list_repository():
+    list_arguments = argparse.Namespace(
+        archive=None,
+        short=None,
+        format=None,
+        json=None,
+        prefix=None,
+        glob_archives=None,
+        sort_by=None,
+        first=None,
+        last=None,
+        find_paths=None,
+    )
+
+    flexmock(module.feature).should_receive('available').and_return(False)
+    flexmock(module.rlist).should_receive('list_repository')
+    flexmock(module.environment).should_receive('make_environment').never()
+    flexmock(module).should_receive('execute_command').never()
+
+    module.list_archive(
+        repository='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        list_arguments=list_arguments,
+    )
+
+
+def test_list_archive_with_borg_features_without_archive_delegates_to_list_repository():
+    list_arguments = argparse.Namespace(
+        archive=None,
+        short=None,
+        format=None,
+        json=None,
+        prefix=None,
+        glob_archives=None,
+        sort_by=None,
+        first=None,
+        last=None,
+        find_paths=None,
+    )
+
+    flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.rlist).should_receive('list_repository')
+    flexmock(module.environment).should_receive('make_environment').never()
+    flexmock(module).should_receive('execute_command').never()
+
+    module.list_archive(
+        repository='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        list_arguments=list_arguments,
+    )
+
+
+@pytest.mark.parametrize(
+    'archive_filter_flag', ('prefix', 'glob_archives', 'sort_by', 'first', 'last',),
+)
+def test_list_archive_with_archive_disallows_archive_filter_flag_if_rlist_feature_available(
+    archive_filter_flag,
+):
+    list_arguments = argparse.Namespace(
+        archive='archive', paths=None, json=False, find_paths=None, **{archive_filter_flag: 'foo'}
+    )
+
+    flexmock(module.feature).should_receive('available').with_args(
+        module.feature.Feature.RLIST, '1.2.3'
+    ).and_return(True)
+
+    with pytest.raises(ValueError):
+        module.list_archive(
+            repository='repo',
+            storage_config={},
+            local_borg_version='1.2.3',
+            list_arguments=list_arguments,
+        )
+
+
+@pytest.mark.parametrize(
+    'archive_filter_flag', ('prefix', 'glob_archives', 'sort_by', 'first', 'last',),
+)
+def test_list_archive_with_archive_allows_archive_filter_flag_if_rlist_feature_unavailable(
+    archive_filter_flag,
+):
+    list_arguments = argparse.Namespace(
+        archive='archive', paths=None, json=False, find_paths=None, **{archive_filter_flag: 'foo'}
+    )
+
+    flexmock(module.feature).should_receive('available').with_args(
+        module.feature.Feature.RLIST, '1.2.3'
+    ).and_return(False)
     flexmock(module).should_receive('make_list_command').with_args(
     flexmock(module).should_receive('make_list_command').with_args(
         repository='repo',
         repository='repo',
         storage_config={},
         storage_config={},
+        local_borg_version='1.2.3',
         list_arguments=list_arguments,
         list_arguments=list_arguments,
         local_path='borg',
         local_path='borg',
         remote_path=None,
         remote_path=None,
@@ -432,6 +526,9 @@ def test_list_archives_calls_borg_with_archive():
         extra_environment=None,
         extra_environment=None,
     ).once()
     ).once()
 
 
-    module.list_archives(
-        repository='repo', storage_config={}, list_arguments=list_arguments,
+    module.list_archive(
+        repository='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        list_arguments=list_arguments,
     )
     )

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

@@ -0,0 +1,381 @@
+import argparse
+import logging
+
+import pytest
+from flexmock import flexmock
+
+from borgmatic.borg import rlist as module
+
+from ..test_verbosity import insert_logging_mock
+
+BORG_LIST_LATEST_ARGUMENTS = (
+    '--last',
+    '1',
+    '--short',
+    'repo',
+)
+
+
+def test_resolve_archive_name_passes_through_non_latest_archive_name():
+    archive = 'myhost-2030-01-01T14:41:17.647620'
+
+    assert (
+        module.resolve_archive_name('repo', archive, storage_config={}, local_borg_version='1.2.3')
+        == archive
+    )
+
+
+def test_resolve_archive_name_calls_borg_with_parameters():
+    expected_archive = 'archive-name'
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
+        output_log_level=None,
+        borg_local_path='borg',
+        extra_environment=None,
+    ).and_return(expected_archive + '\n')
+
+    assert (
+        module.resolve_archive_name('repo', 'latest', storage_config={}, local_borg_version='1.2.3')
+        == expected_archive
+    )
+
+
+def test_resolve_archive_name_with_log_info_calls_borg_with_info_parameter():
+    expected_archive = 'archive-name'
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'list', '--info') + BORG_LIST_LATEST_ARGUMENTS,
+        output_log_level=None,
+        borg_local_path='borg',
+        extra_environment=None,
+    ).and_return(expected_archive + '\n')
+    insert_logging_mock(logging.INFO)
+
+    assert (
+        module.resolve_archive_name('repo', 'latest', storage_config={}, local_borg_version='1.2.3')
+        == expected_archive
+    )
+
+
+def test_resolve_archive_name_with_log_debug_calls_borg_with_debug_parameter():
+    expected_archive = 'archive-name'
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'list', '--debug', '--show-rc') + BORG_LIST_LATEST_ARGUMENTS,
+        output_log_level=None,
+        borg_local_path='borg',
+        extra_environment=None,
+    ).and_return(expected_archive + '\n')
+    insert_logging_mock(logging.DEBUG)
+
+    assert (
+        module.resolve_archive_name('repo', 'latest', storage_config={}, local_borg_version='1.2.3')
+        == expected_archive
+    )
+
+
+def test_resolve_archive_name_with_local_path_calls_borg_via_local_path():
+    expected_archive = 'archive-name'
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg1', 'list') + BORG_LIST_LATEST_ARGUMENTS,
+        output_log_level=None,
+        borg_local_path='borg1',
+        extra_environment=None,
+    ).and_return(expected_archive + '\n')
+
+    assert (
+        module.resolve_archive_name(
+            'repo', 'latest', storage_config={}, local_borg_version='1.2.3', local_path='borg1'
+        )
+        == expected_archive
+    )
+
+
+def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_parameters():
+    expected_archive = 'archive-name'
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'list', '--remote-path', 'borg1') + BORG_LIST_LATEST_ARGUMENTS,
+        output_log_level=None,
+        borg_local_path='borg',
+        extra_environment=None,
+    ).and_return(expected_archive + '\n')
+
+    assert (
+        module.resolve_archive_name(
+            'repo', 'latest', storage_config={}, local_borg_version='1.2.3', remote_path='borg1'
+        )
+        == expected_archive
+    )
+
+
+def test_resolve_archive_name_without_archives_raises():
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
+        output_log_level=None,
+        borg_local_path='borg',
+        extra_environment=None,
+    ).and_return('')
+
+    with pytest.raises(ValueError):
+        module.resolve_archive_name('repo', 'latest', storage_config={}, local_borg_version='1.2.3')
+
+
+def test_resolve_archive_name_with_lock_wait_calls_borg_with_lock_wait_parameters():
+    expected_archive = 'archive-name'
+
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'list', '--lock-wait', 'okay') + BORG_LIST_LATEST_ARGUMENTS,
+        output_log_level=None,
+        borg_local_path='borg',
+        extra_environment=None,
+    ).and_return(expected_archive + '\n')
+
+    assert (
+        module.resolve_archive_name(
+            'repo', 'latest', storage_config={'lock_wait': 'okay'}, local_borg_version='1.2.3'
+        )
+        == expected_archive
+    )
+
+
+def test_make_rlist_command_includes_log_info():
+    insert_logging_mock(logging.INFO)
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    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='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        rlist_arguments=flexmock(archive=None, paths=None, json=False),
+    )
+
+    assert command == ('borg', 'list', '--info', 'repo')
+
+
+def test_make_rlist_command_includes_json_but_not_info():
+    insert_logging_mock(logging.INFO)
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+
+    command = module.make_rlist_command(
+        repository='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        rlist_arguments=flexmock(archive=None, paths=None, json=True),
+    )
+
+    assert command == ('borg', 'list', '--json', 'repo')
+
+
+def test_make_rlist_command_includes_log_debug():
+    insert_logging_mock(logging.DEBUG)
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    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='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        rlist_arguments=flexmock(archive=None, paths=None, json=False),
+    )
+
+    assert command == ('borg', 'list', '--debug', '--show-rc', 'repo')
+
+
+def test_make_rlist_command_includes_json_but_not_debug():
+    insert_logging_mock(logging.DEBUG)
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+
+    command = module.make_rlist_command(
+        repository='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        rlist_arguments=flexmock(archive=None, paths=None, json=True),
+    )
+
+    assert command == ('borg', 'list', '--json', 'repo')
+
+
+def test_make_rlist_command_includes_json():
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+
+    command = module.make_rlist_command(
+        repository='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        rlist_arguments=flexmock(archive=None, paths=None, json=True),
+    )
+
+    assert command == ('borg', 'list', '--json', 'repo')
+
+
+def test_make_rlist_command_includes_lock_wait():
+    flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(
+        ('--lock-wait', '5')
+    )
+    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='repo',
+        storage_config={'lock_wait': 5},
+        local_borg_version='1.2.3',
+        rlist_arguments=flexmock(archive=None, paths=None, json=False),
+    )
+
+    assert command == ('borg', 'list', '--lock-wait', '5', 'repo')
+
+
+def test_make_rlist_command_includes_local_path():
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    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='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        rlist_arguments=flexmock(archive=None, paths=None, json=False),
+        local_path='borg2',
+    )
+
+    assert command == ('borg2', 'list', 'repo')
+
+
+def test_make_rlist_command_includes_remote_path():
+    flexmock(module.flags).should_receive('make_flags').and_return(
+        ('--remote-path', 'borg2')
+    ).and_return(())
+    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='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        rlist_arguments=flexmock(archive=None, paths=None, json=False),
+        remote_path='borg2',
+    )
+
+    assert command == ('borg', 'list', '--remote-path', 'borg2', 'repo')
+
+
+def test_make_rlist_command_includes_short():
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--short',))
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+
+    command = module.make_rlist_command(
+        repository='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        rlist_arguments=flexmock(archive=None, paths=None, json=False, short=True),
+    )
+
+    assert command == ('borg', 'list', '--short', 'repo')
+
+
+@pytest.mark.parametrize(
+    'argument_name',
+    (
+        'prefix',
+        'glob_archives',
+        'sort_by',
+        'first',
+        'last',
+        'exclude',
+        'exclude_from',
+        'pattern',
+        'patterns_from',
+    ),
+)
+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_from_arguments').and_return(
+        (f"--{argument_name.replace('_', '-')}", 'value')
+    )
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+
+    command = module.make_rlist_command(
+        repository='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        rlist_arguments=flexmock(
+            archive=None,
+            paths=None,
+            json=False,
+            find_paths=None,
+            format=None,
+            **{argument_name: 'value'},
+        ),
+    )
+
+    assert command == ('borg', 'list', '--' + argument_name.replace('_', '-'), 'value', 'repo')
+
+
+def test_list_repository_calls_borg_with_parameters():
+    rlist_arguments = argparse.Namespace(json=False)
+
+    flexmock(module.feature).should_receive('available').and_return(False)
+    flexmock(module).should_receive('make_rlist_command').with_args(
+        repository='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        rlist_arguments=rlist_arguments,
+        local_path='borg',
+        remote_path=None,
+    ).and_return(('borg', 'rlist', 'repo'))
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'rlist', 'repo'),
+        output_log_level=logging.WARNING,
+        borg_local_path='borg',
+        extra_environment=None,
+    ).once()
+
+    module.list_repository(
+        repository='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        rlist_arguments=rlist_arguments,
+    )
+
+
+def test_list_repository_with_json_returns_borg_output():
+    rlist_arguments = argparse.Namespace(json=True)
+    json_output = flexmock()
+
+    flexmock(module.feature).should_receive('available').and_return(False)
+    flexmock(module).should_receive('make_rlist_command').with_args(
+        repository='repo',
+        storage_config={},
+        local_borg_version='1.2.3',
+        rlist_arguments=rlist_arguments,
+        local_path='borg',
+        remote_path=None,
+    ).and_return(('borg', 'rlist', 'repo'))
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module).should_receive('execute_command').and_return(json_output)
+
+    assert (
+        module.list_repository(
+            repository='repo',
+            storage_config={},
+            local_borg_version='1.2.3',
+            rlist_arguments=rlist_arguments,
+        )
+        == json_output
+    )

+ 29 - 4
tests/unit/commands/test_borgmatic.py

@@ -571,10 +571,35 @@ def test_run_actions_does_not_raise_for_mount_action():
     )
     )
 
 
 
 
+def test_run_actions_does_not_raise_for_rlist_action():
+    flexmock(module.validate).should_receive('repositories_match').and_return(True)
+    flexmock(module.borg_rlist).should_receive('list_repository')
+    arguments = {
+        'global': flexmock(monitoring_verbosity=1, dry_run=False),
+        'rlist': flexmock(repository=flexmock(), json=flexmock()),
+    }
+
+    list(
+        module.run_actions(
+            arguments=arguments,
+            config_filename='test.yaml',
+            location={'repositories': ['repo']},
+            storage={},
+            retention={},
+            consistency={},
+            hooks={},
+            local_path=None,
+            remote_path=None,
+            local_borg_version=None,
+            repository_path='repo',
+        )
+    )
+
+
 def test_run_actions_does_not_raise_for_list_action():
 def test_run_actions_does_not_raise_for_list_action():
     flexmock(module.validate).should_receive('repositories_match').and_return(True)
     flexmock(module.validate).should_receive('repositories_match').and_return(True)
-    flexmock(module.borg_list).should_receive('resolve_archive_name').and_return(flexmock())
-    flexmock(module.borg_list).should_receive('list_archives')
+    flexmock(module.borg_rlist).should_receive('resolve_archive_name').and_return(flexmock())
+    flexmock(module.borg_list).should_receive('list_archive')
     arguments = {
     arguments = {
         'global': flexmock(monitoring_verbosity=1, dry_run=False),
         'global': flexmock(monitoring_verbosity=1, dry_run=False),
         'list': flexmock(repository=flexmock(), archive=flexmock(), json=flexmock()),
         'list': flexmock(repository=flexmock(), archive=flexmock(), json=flexmock()),
@@ -624,7 +649,7 @@ def test_run_actions_does_not_raise_for_rinfo_action():
 
 
 def test_run_actions_does_not_raise_for_info_action():
 def test_run_actions_does_not_raise_for_info_action():
     flexmock(module.validate).should_receive('repositories_match').and_return(True)
     flexmock(module.validate).should_receive('repositories_match').and_return(True)
-    flexmock(module.borg_list).should_receive('resolve_archive_name').and_return(flexmock())
+    flexmock(module.borg_rlist).should_receive('resolve_archive_name').and_return(flexmock())
     flexmock(module.borg_info).should_receive('display_archives_info')
     flexmock(module.borg_info).should_receive('display_archives_info')
     arguments = {
     arguments = {
         'global': flexmock(monitoring_verbosity=1, dry_run=False),
         'global': flexmock(monitoring_verbosity=1, dry_run=False),
@@ -650,7 +675,7 @@ def test_run_actions_does_not_raise_for_info_action():
 
 
 def test_run_actions_does_not_raise_for_borg_action():
 def test_run_actions_does_not_raise_for_borg_action():
     flexmock(module.validate).should_receive('repositories_match').and_return(True)
     flexmock(module.validate).should_receive('repositories_match').and_return(True)
-    flexmock(module.borg_list).should_receive('resolve_archive_name').and_return(flexmock())
+    flexmock(module.borg_rlist).should_receive('resolve_archive_name').and_return(flexmock())
     flexmock(module.borg_borg).should_receive('run_arbitrary_borg')
     flexmock(module.borg_borg).should_receive('run_arbitrary_borg')
     arguments = {
     arguments = {
         'global': flexmock(monitoring_verbosity=1, dry_run=False),
         'global': flexmock(monitoring_verbosity=1, dry_run=False),