Browse Source

Expand the "borg_extra_options" option to support every Borg sub-command that borgmatic uses (#427).

Dan Helfman 2 tuần trước cách đây
mục cha
commit
f680869d31
40 tập tin đã thay đổi với 621 bổ sung7 xóa
  1. 3 0
      NEWS
  2. 3 0
      borgmatic/borg/break_lock.py
  3. 3 0
      borgmatic/borg/change_passphrase.py
  4. 4 0
      borgmatic/borg/delete.py
  5. 3 0
      borgmatic/borg/export_key.py
  6. 3 0
      borgmatic/borg/export_tar.py
  7. 5 0
      borgmatic/borg/extract.py
  8. 3 0
      borgmatic/borg/import_key.py
  9. 4 0
      borgmatic/borg/info.py
  10. 4 0
      borgmatic/borg/list.py
  11. 3 0
      borgmatic/borg/mount.py
  12. 2 0
      borgmatic/borg/recreate.py
  13. 4 0
      borgmatic/borg/rename.py
  14. 7 0
      borgmatic/borg/repo_create.py
  15. 4 0
      borgmatic/borg/repo_delete.py
  16. 3 0
      borgmatic/borg/repo_info.py
  17. 6 0
      borgmatic/borg/repo_list.py
  18. 3 0
      borgmatic/borg/transfer.py
  19. 3 0
      borgmatic/borg/umount.py
  20. 1 0
      borgmatic/borg/version.py
  21. 94 6
      borgmatic/config/schema.yaml
  22. 1 0
      docs/how-to/run-arbitrary-borg-commands.md
  23. 22 0
      tests/integration/borg/test_rename.py
  24. 12 0
      tests/unit/borg/test_break_lock.py
  25. 17 0
      tests/unit/borg/test_change_passphrase.py
  26. 21 0
      tests/unit/borg/test_delete.py
  27. 14 0
      tests/unit/borg/test_export_key.py
  28. 22 0
      tests/unit/borg/test_export_tar.py
  29. 40 0
      tests/unit/borg/test_extract.py
  30. 15 0
      tests/unit/borg/test_import_key.py
  31. 24 0
      tests/unit/borg/test_info.py
  32. 16 0
      tests/unit/borg/test_list.py
  33. 20 0
      tests/unit/borg/test_mount.py
  34. 36 0
      tests/unit/borg/test_recreate.py
  35. 25 1
      tests/unit/borg/test_repo_create.py
  36. 21 0
      tests/unit/borg/test_repo_delete.py
  37. 42 0
      tests/unit/borg/test_repo_info.py
  38. 63 0
      tests/unit/borg/test_repo_list.py
  39. 35 0
      tests/unit/borg/test_transfer.py
  40. 10 0
      tests/unit/borg/test_umount.py

+ 3 - 0
NEWS

@@ -1,4 +1,7 @@
 2.0.10.dev0
 2.0.10.dev0
+ * #427: Expand the "borg_extra_options" option to support passing arbitrary Borg flags to every
+   Borg sub-command that borgmatic uses. As part of this, deprecate the "init" option under
+   "borg_extra_options" in favor of "repo_create".
  * #942: Factor reference material out of the documentation how-to guides. This means there's now a
  * #942: Factor reference material out of the documentation how-to guides. This means there's now a
    whole reference section in the docs! Check it out: https://torsion.org/borgmatic/
    whole reference section in the docs! Check it out: https://torsion.org/borgmatic/
  * #973: For the MariaDB and MySQL database hooks, add a "skip_names" option to ignore particular
  * #973: For the MariaDB and MySQL database hooks, add a "skip_names" option to ignore particular

+ 3 - 0
borgmatic/borg/break_lock.py

@@ -1,4 +1,5 @@
 import logging
 import logging
+import shlex
 
 
 import borgmatic.config.paths
 import borgmatic.config.paths
 from borgmatic.borg import environment, flags
 from borgmatic.borg import environment, flags
@@ -22,6 +23,7 @@ def break_lock(
     '''
     '''
     umask = config.get('umask', None)
     umask = config.get('umask', None)
     lock_wait = config.get('lock_wait', None)
     lock_wait = config.get('lock_wait', None)
+    extra_borg_options = config.get('extra_borg_options', {}).get('break_lock', '')
 
 
     full_command = (
     full_command = (
         (local_path, 'break-lock')
         (local_path, 'break-lock')
@@ -31,6 +33,7 @@ def break_lock(
         + (('--lock-wait', str(lock_wait)) if lock_wait else ())
         + (('--lock-wait', str(lock_wait)) if lock_wait else ())
         + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
         + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
         + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
         + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+        + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
         + flags.make_repository_flags(repository_path, local_borg_version)
         + flags.make_repository_flags(repository_path, local_borg_version)
     )
     )
 
 

+ 3 - 0
borgmatic/borg/change_passphrase.py

@@ -1,4 +1,5 @@
 import logging
 import logging
+import shlex
 
 
 import borgmatic.config.paths
 import borgmatic.config.paths
 import borgmatic.execute
 import borgmatic.execute
@@ -25,6 +26,7 @@ def change_passphrase(
     borgmatic.logger.add_custom_log_levels()
     borgmatic.logger.add_custom_log_levels()
     umask = config.get('umask', None)
     umask = config.get('umask', None)
     lock_wait = config.get('lock_wait', None)
     lock_wait = config.get('lock_wait', None)
+    extra_borg_options = config.get('extra_borg_options', {}).get('key_change_passphrase', '')
 
 
     full_command = (
     full_command = (
         (local_path, 'key', 'change-passphrase')
         (local_path, 'key', 'change-passphrase')
@@ -34,6 +36,7 @@ def change_passphrase(
         + (('--lock-wait', str(lock_wait)) if lock_wait else ())
         + (('--lock-wait', str(lock_wait)) if lock_wait else ())
         + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
         + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
         + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
         + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+        + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
         + flags.make_repository_flags(
         + flags.make_repository_flags(
             repository_path,
             repository_path,
             local_borg_version,
             local_borg_version,

+ 4 - 0
borgmatic/borg/delete.py

@@ -1,5 +1,6 @@
 import argparse
 import argparse
 import logging
 import logging
+import shlex
 
 
 import borgmatic.borg.environment
 import borgmatic.borg.environment
 import borgmatic.borg.feature
 import borgmatic.borg.feature
@@ -28,6 +29,8 @@ def make_delete_command(
     arguments to the delete action as an argparse.Namespace, and global arguments, return a command
     arguments to the delete action as an argparse.Namespace, and global arguments, return a command
     as a tuple to delete archives from the repository.
     as a tuple to delete archives from the repository.
     '''
     '''
+    extra_borg_options = config.get('extra_borg_options', {}).get('delete', '')
+
     return (
     return (
         (local_path, 'delete')
         (local_path, 'delete')
         + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
         + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
@@ -66,6 +69,7 @@ def make_delete_command(
                 'repository',
                 'repository',
             ),
             ),
         )
         )
+        + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
         + borgmatic.borg.flags.make_repository_flags(repository['path'], local_borg_version)
         + borgmatic.borg.flags.make_repository_flags(repository['path'], local_borg_version)
     )
     )
 
 

+ 3 - 0
borgmatic/borg/export_key.py

@@ -1,5 +1,6 @@
 import logging
 import logging
 import os
 import os
+import shlex
 
 
 import borgmatic.config.paths
 import borgmatic.config.paths
 import borgmatic.logger
 import borgmatic.logger
@@ -31,6 +32,7 @@ def export_key(
     umask = config.get('umask', None)
     umask = config.get('umask', None)
     lock_wait = config.get('lock_wait', None)
     lock_wait = config.get('lock_wait', None)
     working_directory = borgmatic.config.paths.get_working_directory(config)
     working_directory = borgmatic.config.paths.get_working_directory(config)
+    extra_borg_options = config.get('extra_borg_options', {}).get('key_export', '')
 
 
     if export_arguments.path and export_arguments.path != '-':
     if export_arguments.path and export_arguments.path != '-':
         if os.path.exists(os.path.join(working_directory or '', export_arguments.path)):
         if os.path.exists(os.path.join(working_directory or '', export_arguments.path)):
@@ -52,6 +54,7 @@ def export_key(
         + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
         + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
         + flags.make_flags('paper', export_arguments.paper)
         + flags.make_flags('paper', export_arguments.paper)
         + flags.make_flags('qr-html', export_arguments.qr_html)
         + flags.make_flags('qr-html', export_arguments.qr_html)
+        + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
         + flags.make_repository_flags(
         + flags.make_repository_flags(
             repository_path,
             repository_path,
             local_borg_version,
             local_borg_version,

+ 3 - 0
borgmatic/borg/export_tar.py

@@ -1,4 +1,5 @@
 import logging
 import logging
+import shlex
 
 
 import borgmatic.config.paths
 import borgmatic.config.paths
 import borgmatic.logger
 import borgmatic.logger
@@ -34,6 +35,7 @@ def export_tar_archive(
     borgmatic.logger.add_custom_log_levels()
     borgmatic.logger.add_custom_log_levels()
     umask = config.get('umask', None)
     umask = config.get('umask', None)
     lock_wait = config.get('lock_wait', None)
     lock_wait = config.get('lock_wait', None)
+    extra_borg_options = config.get('extra_borg_options', {}).get('export_tar', '')
 
 
     full_command = (
     full_command = (
         (local_path, 'export-tar')
         (local_path, 'export-tar')
@@ -47,6 +49,7 @@ def export_tar_archive(
         + (('--dry-run',) if dry_run else ())
         + (('--dry-run',) if dry_run else ())
         + (('--tar-filter', tar_filter) if tar_filter else ())
         + (('--tar-filter', tar_filter) if tar_filter else ())
         + (('--strip-components', str(strip_components)) if strip_components else ())
         + (('--strip-components', str(strip_components)) if strip_components else ())
+        + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
         + flags.make_repository_archive_flags(
         + flags.make_repository_archive_flags(
             repository_path,
             repository_path,
             archive,
             archive,

+ 5 - 0
borgmatic/borg/extract.py

@@ -1,5 +1,6 @@
 import logging
 import logging
 import os
 import os
+import shlex
 import subprocess
 import subprocess
 
 
 import borgmatic.config.paths
 import borgmatic.config.paths
@@ -23,6 +24,7 @@ def extract_last_archive_dry_run(
     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
     dry-run.
     dry-run.
     '''
     '''
+    extra_borg_options = config.get('extra_borg_options', {}).get('extract', '')
     verbosity_flags = ()
     verbosity_flags = ()
     if logger.isEnabledFor(logging.DEBUG):
     if logger.isEnabledFor(logging.DEBUG):
         verbosity_flags = ('--debug', '--show-rc')
         verbosity_flags = ('--debug', '--show-rc')
@@ -51,6 +53,7 @@ def extract_last_archive_dry_run(
         + (('--lock-wait', str(lock_wait)) if lock_wait else ())
         + (('--lock-wait', str(lock_wait)) if lock_wait else ())
         + verbosity_flags
         + verbosity_flags
         + list_flag
         + list_flag
+        + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
         + flags.make_repository_archive_flags(
         + flags.make_repository_archive_flags(
             repository_path,
             repository_path,
             last_archive_name,
             last_archive_name,
@@ -92,6 +95,7 @@ def extract_archive(
     '''
     '''
     umask = config.get('umask', None)
     umask = config.get('umask', None)
     lock_wait = config.get('lock_wait', None)
     lock_wait = config.get('lock_wait', None)
+    extra_borg_options = config.get('extra_borg_options', {}).get('extract', '')
 
 
     if config.get('progress') and extract_to_stdout:
     if config.get('progress') and extract_to_stdout:
         raise ValueError('progress and extract to stdout cannot both be set')
         raise ValueError('progress and extract to stdout cannot both be set')
@@ -131,6 +135,7 @@ def extract_archive(
         + (('--strip-components', str(strip_components)) if strip_components else ())
         + (('--strip-components', str(strip_components)) if strip_components else ())
         + (('--progress',) if config.get('progress') else ())
         + (('--progress',) if config.get('progress') else ())
         + (('--stdout',) if extract_to_stdout else ())
         + (('--stdout',) if extract_to_stdout else ())
+        + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
         + flags.make_repository_archive_flags(
         + flags.make_repository_archive_flags(
             # Make the repository path absolute so the destination directory used below via changing
             # Make the repository path absolute so the destination directory used below via changing
             # the working directory doesn't prevent Borg from finding the repo. But also apply the
             # the working directory doesn't prevent Borg from finding the repo. But also apply the

+ 3 - 0
borgmatic/borg/import_key.py

@@ -1,5 +1,6 @@
 import logging
 import logging
 import os
 import os
+import shlex
 
 
 import borgmatic.config.paths
 import borgmatic.config.paths
 import borgmatic.logger
 import borgmatic.logger
@@ -30,6 +31,7 @@ def import_key(
     umask = config.get('umask', None)
     umask = config.get('umask', None)
     lock_wait = config.get('lock_wait', None)
     lock_wait = config.get('lock_wait', None)
     working_directory = borgmatic.config.paths.get_working_directory(config)
     working_directory = borgmatic.config.paths.get_working_directory(config)
+    extra_borg_options = config.get('extra_borg_options', {}).get('key_import', '')
 
 
     if import_arguments.path and import_arguments.path != '-':
     if import_arguments.path and import_arguments.path != '-':
         if not os.path.exists(os.path.join(working_directory or '', import_arguments.path)):
         if not os.path.exists(os.path.join(working_directory or '', import_arguments.path)):
@@ -48,6 +50,7 @@ def import_key(
         + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
         + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
         + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
         + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
         + flags.make_flags('paper', import_arguments.paper)
         + flags.make_flags('paper', import_arguments.paper)
+        + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
         + flags.make_repository_flags(
         + flags.make_repository_flags(
             repository_path,
             repository_path,
             local_borg_version,
             local_borg_version,

+ 4 - 0
borgmatic/borg/info.py

@@ -1,5 +1,6 @@
 import argparse
 import argparse
 import logging
 import logging
+import shlex
 
 
 import borgmatic.config.paths
 import borgmatic.config.paths
 import borgmatic.logger
 import borgmatic.logger
@@ -23,6 +24,8 @@ def make_info_command(
     arguments to the info action as an argparse.Namespace, and global arguments, return a command
     arguments to the info action as an argparse.Namespace, and global arguments, return a command
     as a tuple to display summary information for archives in the repository.
     as a tuple to display summary information for archives in the repository.
     '''
     '''
+    extra_borg_options = config.get('extra_borg_options', {}).get('info', '')
+
     return (
     return (
         (local_path, 'info')
         (local_path, 'info')
         + (
         + (
@@ -58,6 +61,7 @@ def make_info_command(
             info_arguments,
             info_arguments,
             excludes=('repository', 'archive', 'prefix', 'match_archives'),
             excludes=('repository', 'archive', 'prefix', 'match_archives'),
         )
         )
+        + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
         + flags.make_repository_flags(repository_path, local_borg_version)
         + flags.make_repository_flags(repository_path, local_borg_version)
     )
     )
 
 

+ 4 - 0
borgmatic/borg/list.py

@@ -2,6 +2,7 @@ import argparse
 import copy
 import copy
 import logging
 import logging
 import re
 import re
+import shlex
 
 
 import borgmatic.config.paths
 import borgmatic.config.paths
 import borgmatic.logger
 import borgmatic.logger
@@ -35,6 +36,8 @@ def make_list_command(
     and local and remote Borg paths, return a command as a tuple to list archives or paths within an
     and local and remote Borg paths, return a command as a tuple to list archives or paths within an
     archive.
     archive.
     '''
     '''
+    extra_borg_options = config.get('extra_borg_options', {}).get('list', '')
+
     return (
     return (
         (local_path, 'list')
         (local_path, 'list')
         + (
         + (
@@ -52,6 +55,7 @@ def make_list_command(
         + flags.make_flags('log-json', config.get('log_json'))
         + flags.make_flags('log-json', config.get('log_json'))
         + flags.make_flags('lock-wait', config.get('lock_wait'))
         + flags.make_flags('lock-wait', config.get('lock_wait'))
         + flags.make_flags_from_arguments(list_arguments, excludes=MAKE_FLAGS_EXCLUDES)
         + flags.make_flags_from_arguments(list_arguments, excludes=MAKE_FLAGS_EXCLUDES)
+        + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
         + (
         + (
             flags.make_repository_archive_flags(
             flags.make_repository_archive_flags(
                 repository_path,
                 repository_path,

+ 3 - 0
borgmatic/borg/mount.py

@@ -1,4 +1,5 @@
 import logging
 import logging
+import shlex
 
 
 import borgmatic.config.paths
 import borgmatic.config.paths
 from borgmatic.borg import environment, feature, flags
 from borgmatic.borg import environment, feature, flags
@@ -25,6 +26,7 @@ def mount_archive(
     '''
     '''
     umask = config.get('umask', None)
     umask = config.get('umask', None)
     lock_wait = config.get('lock_wait', None)
     lock_wait = config.get('lock_wait', None)
+    extra_borg_options = config.get('extra_borg_options', {}).get('mount', '')
 
 
     full_command = (
     full_command = (
         (local_path, 'mount')
         (local_path, 'mount')
@@ -39,6 +41,7 @@ def mount_archive(
             excludes=('repository', 'archive', 'mount_point', 'paths', 'options'),
             excludes=('repository', 'archive', 'mount_point', 'paths', 'options'),
         )
         )
         + (('-o', mount_arguments.options) if mount_arguments.options else ())
         + (('-o', mount_arguments.options) if mount_arguments.options else ())
+        + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
         + (
         + (
             (
             (
                 flags.make_repository_flags(repository_path, local_borg_version)
                 flags.make_repository_flags(repository_path, local_borg_version)

+ 2 - 0
borgmatic/borg/recreate.py

@@ -32,6 +32,7 @@ def recreate_archive(
     exclude_flags = flags.make_exclude_flags(config)
     exclude_flags = flags.make_exclude_flags(config)
     compression = config.get('compression', None)
     compression = config.get('compression', None)
     chunker_params = config.get('chunker_params', None)
     chunker_params = config.get('chunker_params', None)
+    extra_borg_options = config.get('extra_borg_options', {}).get('recreate', '')
 
 
     # Available recompress MODES: "if-different", "always", "never" (default)
     # Available recompress MODES: "if-different", "always", "never" (default)
     recompress = config.get('recompress', None)
     recompress = config.get('recompress', None)
@@ -71,6 +72,7 @@ def recreate_archive(
         + (('--chunker-params', chunker_params) if chunker_params else ())
         + (('--chunker-params', chunker_params) if chunker_params else ())
         + (('--recompress', recompress) if recompress else ())
         + (('--recompress', recompress) if recompress else ())
         + exclude_flags
         + exclude_flags
+        + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
         + (
         + (
             (
             (
                 flags.make_repository_flags(repository, local_borg_version)
                 flags.make_repository_flags(repository, local_borg_version)

+ 4 - 0
borgmatic/borg/rename.py

@@ -1,4 +1,5 @@
 import logging
 import logging
+import shlex
 
 
 import borgmatic.borg.flags
 import borgmatic.borg.flags
 
 
@@ -15,6 +16,8 @@ def make_rename_command(
     local_path,
     local_path,
     remote_path,
     remote_path,
 ):
 ):
+    extra_borg_options = config.get('extra_borg_options', {}).get('rename', '')
+
     return (
     return (
         (local_path, 'rename')
         (local_path, 'rename')
         + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
         + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
@@ -24,6 +27,7 @@ def make_rename_command(
         + borgmatic.borg.flags.make_flags('umask', config.get('umask'))
         + borgmatic.borg.flags.make_flags('umask', config.get('umask'))
         + borgmatic.borg.flags.make_flags('log-json', config.get('log_json'))
         + borgmatic.borg.flags.make_flags('log-json', config.get('log_json'))
         + borgmatic.borg.flags.make_flags('lock-wait', config.get('lock_wait'))
         + borgmatic.borg.flags.make_flags('lock-wait', config.get('lock_wait'))
+        + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
         + borgmatic.borg.flags.make_repository_archive_flags(
         + borgmatic.borg.flags.make_repository_archive_flags(
             repository_name,
             repository_name,
             old_archive_name,
             old_archive_name,

+ 7 - 0
borgmatic/borg/repo_create.py

@@ -66,8 +66,14 @@ def create_repository(
 
 
     lock_wait = config.get('lock_wait')
     lock_wait = config.get('lock_wait')
     umask = config.get('umask')
     umask = config.get('umask')
+    extra_borg_options_from_init = config.get('extra_borg_options', {}).get('init', '')
     extra_borg_options = config.get('extra_borg_options', {}).get('repo-create', '')
     extra_borg_options = config.get('extra_borg_options', {}).get('repo-create', '')
 
 
+    if extra_borg_options_from_init:
+        logger.warning(
+            'The "init" option in "extra_borg_options" is deprecated and will be removed from a future release; use "repo_create" instead.'
+        )
+
     repo_create_command = (
     repo_create_command = (
         (local_path,)
         (local_path,)
         + (
         + (
@@ -88,6 +94,7 @@ def create_repository(
         + (('--remote-path', remote_path) if remote_path else ())
         + (('--remote-path', remote_path) if remote_path else ())
         + (('--umask', str(umask)) if umask else ())
         + (('--umask', str(umask)) if umask else ())
         + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
         + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
+        + (tuple(shlex.split(extra_borg_options_from_init)) if extra_borg_options_from_init else ())
         + flags.make_repository_flags(repository_path, local_borg_version)
         + flags.make_repository_flags(repository_path, local_borg_version)
     )
     )
 
 

+ 4 - 0
borgmatic/borg/repo_delete.py

@@ -1,4 +1,5 @@
 import logging
 import logging
+import shlex
 
 
 import borgmatic.borg.environment
 import borgmatic.borg.environment
 import borgmatic.borg.feature
 import borgmatic.borg.feature
@@ -26,6 +27,8 @@ def make_repo_delete_command(
     arguments to the repo_delete action as an argparse.Namespace, and global arguments, return a command
     arguments to the repo_delete action as an argparse.Namespace, and global arguments, return a command
     as a tuple to repo_delete the entire repository.
     as a tuple to repo_delete the entire repository.
     '''
     '''
+    extra_borg_options = config.get('extra_borg_options', {}).get('repo_delete', '')
+
     return (
     return (
         (local_path,)
         (local_path,)
         + (
         + (
@@ -56,6 +59,7 @@ def make_repo_delete_command(
             repo_delete_arguments,
             repo_delete_arguments,
             excludes=('list_details', 'force', 'repository'),
             excludes=('list_details', 'force', 'repository'),
         )
         )
+        + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
         + borgmatic.borg.flags.make_repository_flags(repository['path'], local_borg_version)
         + borgmatic.borg.flags.make_repository_flags(repository['path'], local_borg_version)
     )
     )
 
 

+ 3 - 0
borgmatic/borg/repo_info.py

@@ -1,4 +1,5 @@
 import logging
 import logging
+import shlex
 
 
 import borgmatic.config.paths
 import borgmatic.config.paths
 import borgmatic.logger
 import borgmatic.logger
@@ -24,6 +25,7 @@ def display_repository_info(
     '''
     '''
     borgmatic.logger.add_custom_log_levels()
     borgmatic.logger.add_custom_log_levels()
     lock_wait = config.get('lock_wait', None)
     lock_wait = config.get('lock_wait', None)
+    extra_borg_options = config.get('extra_borg_options', {}).get('repo_info', '')
 
 
     full_command = (
     full_command = (
         (local_path,)
         (local_path,)
@@ -47,6 +49,7 @@ def display_repository_info(
         + flags.make_flags('log-json', config.get('log_json'))
         + flags.make_flags('log-json', config.get('log_json'))
         + flags.make_flags('lock-wait', lock_wait)
         + flags.make_flags('lock-wait', lock_wait)
         + (('--json',) if repo_info_arguments.json else ())
         + (('--json',) if repo_info_arguments.json else ())
+        + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
         + flags.make_repository_flags(repository_path, local_borg_version)
         + flags.make_repository_flags(repository_path, local_borg_version)
     )
     )
 
 

+ 6 - 0
borgmatic/borg/repo_list.py

@@ -1,6 +1,7 @@
 import argparse
 import argparse
 import json
 import json
 import logging
 import logging
+import shlex
 
 
 import borgmatic.config.paths
 import borgmatic.config.paths
 import borgmatic.logger
 import borgmatic.logger
@@ -62,6 +63,7 @@ def get_latest_archive(
 
 
     Raises ValueError if there are no archives in the repository.
     Raises ValueError if there are no archives in the repository.
     '''
     '''
+    extra_borg_options = config.get('extra_borg_options', {}).get('repo_list', '')
 
 
     full_command = (
     full_command = (
         local_path,
         local_path,
@@ -81,6 +83,7 @@ def get_latest_archive(
         ),
         ),
         *flags.make_flags('last', 1),
         *flags.make_flags('last', 1),
         '--json',
         '--json',
+        *(tuple(shlex.split(extra_borg_options)) if extra_borg_options else ()),
         *flags.make_repository_flags(repository_path, local_borg_version),
         *flags.make_repository_flags(repository_path, local_borg_version),
     )
     )
 
 
@@ -121,6 +124,8 @@ def make_repo_list_command(
     arguments to the repo_list action, global arguments as an argparse.Namespace instance, and local and
     arguments to the repo_list action, global arguments as an argparse.Namespace instance, and local and
     remote Borg paths, return a command as a tuple to list archives with a repository.
     remote Borg paths, return a command as a tuple to list archives with a repository.
     '''
     '''
+    extra_borg_options = config.get('extra_borg_options', {}).get('repo_list', '')
+
     return (
     return (
         (
         (
             local_path,
             local_path,
@@ -160,6 +165,7 @@ def make_repo_list_command(
             )
             )
         )
         )
         + flags.make_flags_from_arguments(repo_list_arguments, excludes=MAKE_FLAGS_EXCLUDES)
         + flags.make_flags_from_arguments(repo_list_arguments, excludes=MAKE_FLAGS_EXCLUDES)
+        + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
         + flags.make_repository_flags(repository_path, local_borg_version)
         + flags.make_repository_flags(repository_path, local_borg_version)
     )
     )
 
 

+ 3 - 0
borgmatic/borg/transfer.py

@@ -1,4 +1,5 @@
 import logging
 import logging
+import shlex
 
 
 import borgmatic.config.paths
 import borgmatic.config.paths
 import borgmatic.logger
 import borgmatic.logger
@@ -24,6 +25,7 @@ def transfer_archives(
     instance, transfer archives to the given repository.
     instance, transfer archives to the given repository.
     '''
     '''
     borgmatic.logger.add_custom_log_levels()
     borgmatic.logger.add_custom_log_levels()
+    extra_borg_options = config.get('extra_borg_options', {}).get('transfer', '')
 
 
     full_command = (
     full_command = (
         (local_path, 'transfer')
         (local_path, 'transfer')
@@ -53,6 +55,7 @@ def transfer_archives(
                 )
                 )
             )
             )
         )
         )
+        + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
         + flags.make_repository_flags(repository_path, local_borg_version)
         + flags.make_repository_flags(repository_path, local_borg_version)
         + flags.make_flags('other-repo', transfer_arguments.source_repository)
         + flags.make_flags('other-repo', transfer_arguments.source_repository)
         + flags.make_flags('dry-run', dry_run)
         + flags.make_flags('dry-run', dry_run)

+ 3 - 0
borgmatic/borg/umount.py

@@ -1,4 +1,5 @@
 import logging
 import logging
+import shlex
 
 
 import borgmatic.config.paths
 import borgmatic.config.paths
 from borgmatic.execute import execute_command
 from borgmatic.execute import execute_command
@@ -11,10 +12,12 @@ def unmount_archive(config, mount_point, local_path='borg'):
     Given a mounted filesystem mount point, and an optional local Borg paths, umount the filesystem
     Given a mounted filesystem mount point, and an optional local Borg paths, umount the filesystem
     from the mount point.
     from the mount point.
     '''
     '''
+    extra_borg_options = config.get('extra_borg_options', {}).get('umount', '')
     full_command = (
     full_command = (
         (local_path, 'umount')
         (local_path, 'umount')
         + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
         + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
         + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
         + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+        + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
         + (mount_point,)
         + (mount_point,)
     )
     )
 
 

+ 1 - 0
borgmatic/borg/version.py

@@ -19,6 +19,7 @@ def local_borg_version(config, local_path='borg'):
         + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
         + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
         + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
         + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
     )
     )
+
     output = execute_command_and_capture_output(
     output = execute_command_and_capture_output(
         full_command,
         full_command,
         environment=environment.make_environment(config),
         environment=environment.make_environment(config),

+ 94 - 6
borgmatic/config/schema.yaml

@@ -525,30 +525,118 @@ properties:
         type: object
         type: object
         additionalProperties: false
         additionalProperties: false
         properties:
         properties:
-            init:
+            break_lock:
+                type: string
+                description: |
+                  Extra command-line options to pass to "borg break-lock".
+                example: "--extra-option"
+            check:
+                type: string
+                description: |
+                  Extra command-line options to pass to "borg check".
+                example: "--extra-option"
+            compact:
                 type: string
                 type: string
                 description: |
                 description: |
-                  Extra command-line options to pass to "borg init".
+                  Extra command-line options to pass to "borg compact".
                 example: "--extra-option"
                 example: "--extra-option"
             create:
             create:
                 type: string
                 type: string
                 description: |
                 description: |
                   Extra command-line options to pass to "borg create".
                   Extra command-line options to pass to "borg create".
                 example: "--extra-option"
                 example: "--extra-option"
+            delete:
+                type: string
+                description: |
+                  Extra command-line options to pass to "borg delete".
+                example: "--extra-option"
+            export_tar:
+                type: string
+                description: |
+                  Extra command-line options to pass to "borg export-tar".
+                example: "--extra-option"
+            extract:
+                type: string
+                description: |
+                  Extra command-line options to pass to "borg extract".
+                example: "--extra-option"
+            key_export:
+                type: string
+                description: |
+                  Extra command-line options to pass to "borg key export".
+                example: "--extra-option"
+            key_import:
+                type: string
+                description: |
+                  Extra command-line options to pass to "borg key import".
+                example: "--extra-option"
+            key_change_passphrase:
+                type: string
+                description: |
+                  Extra command-line options to pass to "borg key
+                  change-passphrase".
+                example: "--extra-option"
+            info:
+                type: string
+                description: |
+                  Extra command-line options to pass to "borg info".
+                example: "--extra-option"
+            init:
+                type: string
+                description: |
+                  Deprecated. Use "repo_create" instead. Extra command-line
+                  options to pass to "borg init" / "borg repo-create".
+                example: "--extra-option"
+            list:
+                type: string
+                description: |
+                  Extra command-line options to pass to "borg list".
+                example: "--extra-option"
+            mount:
+                type: string
+                description: |
+                  Extra command-line options to pass to "borg mount".
+                example: "--extra-option"
             prune:
             prune:
                 type: string
                 type: string
                 description: |
                 description: |
                   Extra command-line options to pass to "borg prune".
                   Extra command-line options to pass to "borg prune".
                 example: "--extra-option"
                 example: "--extra-option"
-            compact:
+            recreate:
                 type: string
                 type: string
                 description: |
                 description: |
-                  Extra command-line options to pass to "borg compact".
+                  Extra command-line options to pass to "borg recreate".
                 example: "--extra-option"
                 example: "--extra-option"
-            check:
+            repo_create:
                 type: string
                 type: string
                 description: |
                 description: |
-                  Extra command-line options to pass to "borg check".
+                  Extra command-line options to pass to "borg init" / "borg
+                  repo-create".
+                example: "--extra-option"
+            repo_delete:
+                type: string
+                description: |
+                  Extra command-line options to pass to "borg repo-delete".
+                example: "--extra-option"
+            repo_info:
+                type: string
+                description: |
+                  Extra command-line options to pass to "borg repo-info".
+                example: "--extra-option"
+            repo_list:
+                type: string
+                description: |
+                  Extra command-line options to pass to "borg repo-list".
+                example: "--extra-option"
+            transfer:
+                type: string
+                description: |
+                  Extra command-line options to pass to "borg transfer".
+                example: "--extra-option"
+            umount:
+                type: string
+                description: |
+                  Extra command-line options to pass to "borg umount".
                 example: "--extra-option"
                 example: "--extra-option"
         description: |
         description: |
             Additional options to pass directly to particular Borg commands,
             Additional options to pass directly to particular Borg commands,

+ 1 - 0
docs/how-to/run-arbitrary-borg-commands.md

@@ -134,6 +134,7 @@ borgmatic's `borg` action is not without limitations:
    not disable certain borgmatic logs to avoid interfering with JSON output.
    not disable certain borgmatic logs to avoid interfering with JSON output.
  * The `borg` action bypasses most of borgmatic's machinery, so for instance
  * The `borg` action bypasses most of borgmatic's machinery, so for instance
    monitoring hooks will not get triggered when running `borgmatic borg create`.
    monitoring hooks will not get triggered when running `borgmatic borg create`.
+ * The `borg` action does not use `extra_borg_options`.
  * <span class="minilink minilink-addedin">Prior to version 1.8.0</span>
  * <span class="minilink minilink-addedin">Prior to version 1.8.0</span>
    borgmatic implicitly injected the repository/archive arguments on the Borg
    borgmatic implicitly injected the repository/archive arguments on the Borg
    command-line for you (based on your borgmatic configuration or the
    command-line for you (based on your borgmatic configuration or the

+ 22 - 0
tests/integration/borg/test_rename.py

@@ -118,3 +118,25 @@ def test_make_rename_command_includes_lock_wait():
     )
     )
 
 
     assert command == ('borg', 'rename', '--lock-wait', '5', 'repo::old_archive', 'new_archive')
     assert command == ('borg', 'rename', '--lock-wait', '5', 'repo::old_archive', 'new_archive')
+
+
+def test_make_rename_command_includes_extra_borg_options():
+    command = module.make_rename_command(
+        dry_run=False,
+        repository_name='repo',
+        old_archive_name='old_archive',
+        new_archive_name='new_archive',
+        config={'extra_borg_options': {'rename': '--extra "value with space"'}},
+        local_borg_version='1.2.3',
+        local_path='borg',
+        remote_path=None,
+    )
+
+    assert command == (
+        'borg',
+        'rename',
+        '--extra',
+        'value with space',
+        'repo::old_archive',
+        'new_archive',
+    )

+ 12 - 0
tests/unit/borg/test_break_lock.py

@@ -108,6 +108,18 @@ def test_break_lock_calls_borg_with_lock_wait_flags():
     )
     )
 
 
 
 
+def test_break_lock_calls_borg_with_extra_borg_options():
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    insert_execute_command_mock(('borg', 'break-lock', '--extra', 'value with space', 'repo'))
+
+    module.break_lock(
+        repository_path='repo',
+        config={'extra_borg_options': {'break_lock': '--extra "value with space"'}},
+        local_borg_version='1.2.3',
+        global_arguments=flexmock(),
+    )
+
+
 def test_break_lock_with_log_info_calls_borg_with_info_parameter():
 def test_break_lock_with_log_info_calls_borg_with_info_parameter():
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     insert_execute_command_mock(('borg', 'break-lock', '--info', 'repo'))
     insert_execute_command_mock(('borg', 'break-lock', '--info', 'repo'))

+ 17 - 0
tests/unit/borg/test_change_passphrase.py

@@ -145,6 +145,23 @@ def test_change_passphrase_calls_borg_with_lock_wait_flags():
     )
     )
 
 
 
 
+def test_change_passphrase_calls_borg_with_extra_borg_options():
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    config = {'extra_borg_options': {'key_change_passphrase': '--extra "value with space"'}}
+    insert_execute_command_mock(
+        ('borg', 'key', 'change-passphrase', '--extra', 'value with space', 'repo'),
+        config=config,
+    )
+
+    module.change_passphrase(
+        repository_path='repo',
+        config=config,
+        local_borg_version='1.2.3',
+        change_passphrase_arguments=flexmock(),
+        global_arguments=flexmock(dry_run=False),
+    )
+
+
 def test_change_passphrase_with_log_info_calls_borg_with_info_parameter():
 def test_change_passphrase_with_log_info_calls_borg_with_info_parameter():
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     insert_execute_command_mock(('borg', 'key', 'change-passphrase', '--info', 'repo'))
     insert_execute_command_mock(('borg', 'key', 'change-passphrase', '--info', 'repo'))

+ 21 - 0
tests/unit/borg/test_delete.py

@@ -175,6 +175,27 @@ def test_make_delete_command_includes_lock_wait():
     assert command == ('borg', 'delete', '--lock-wait', '5', 'repo')
     assert command == ('borg', 'delete', '--lock-wait', '5', 'repo')
 
 
 
 
+def test_make_delete_command_includes_extra_borg_options():
+    flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
+    flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
+    flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
+    flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
+        ('repo',),
+    )
+
+    command = module.make_delete_command(
+        repository={'path': 'repo'},
+        config={'extra_borg_options': {'delete': '--extra "value with space"'}},
+        local_borg_version='1.2.3',
+        delete_arguments=flexmock(list_details=False, force=0, match_archives=None, archive=None),
+        global_arguments=flexmock(dry_run=False),
+        local_path='borg',
+        remote_path=None,
+    )
+
+    assert command == ('borg', 'delete', '--extra', 'value with space', 'repo')
+
+
 def test_make_delete_command_with_list_config_calls_borg_with_list_flag():
 def test_make_delete_command_with_list_config_calls_borg_with_list_flag():
     flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
     flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
     flexmock(module.borgmatic.borg.flags).should_receive('make_flags').with_args(
     flexmock(module.borgmatic.borg.flags).should_receive('make_flags').with_args(

+ 14 - 0
tests/unit/borg/test_export_key.py

@@ -133,6 +133,20 @@ def test_export_key_calls_borg_with_lock_wait_flags():
     )
     )
 
 
 
 
+def test_export_key_calls_borg_with_extra_borg_options():
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    flexmock(module.os.path).should_receive('exists').never()
+    insert_execute_command_mock(('borg', 'key', 'export', '--extra', 'value with space', 'repo'))
+
+    module.export_key(
+        repository_path='repo',
+        config={'extra_borg_options': {'key_export': '--extra "value with space"'}},
+        local_borg_version='1.2.3',
+        export_arguments=flexmock(paper=False, qr_html=False, path=None),
+        global_arguments=flexmock(dry_run=False),
+    )
+
+
 def test_export_key_with_log_info_calls_borg_with_info_parameter():
 def test_export_key_with_log_info_calls_borg_with_info_parameter():
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.os.path).should_receive('exists').never()
     flexmock(module.os.path).should_receive('exists').never()

+ 22 - 0
tests/unit/borg/test_export_tar.py

@@ -187,6 +187,28 @@ def test_export_tar_archive_calls_borg_with_lock_wait_flags():
     )
     )
 
 
 
 
+def test_export_tar_archive_calls_borg_with_extra_borg_options():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+    flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
+        ('repo::archive',),
+    )
+    insert_execute_command_mock(
+        ('borg', 'export-tar', '--extra', 'value with space', 'repo::archive', 'test.tar'),
+    )
+
+    module.export_tar_archive(
+        dry_run=False,
+        repository_path='repo',
+        archive='archive',
+        paths=None,
+        destination_path='test.tar',
+        config={'extra_borg_options': {'export_tar': '--extra "value with space"'}},
+        local_borg_version='1.2.3',
+        global_arguments=flexmock(),
+    )
+
+
 def test_export_tar_archive_with_log_info_calls_borg_with_info_flag():
 def test_export_tar_archive_with_log_info_calls_borg_with_info_flag():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER

+ 40 - 0
tests/unit/borg/test_extract.py

@@ -174,6 +174,23 @@ def test_extract_last_archive_dry_run_calls_borg_with_lock_wait_flags():
     )
     )
 
 
 
 
+def test_extract_last_archive_dry_run_calls_borg_with_extra_borg_options():
+    flexmock(module.repo_list).should_receive('resolve_archive_name').and_return('archive')
+    insert_execute_command_mock(
+        ('borg', 'extract', '--dry-run', '--extra', 'value with space', 'repo::archive'),
+    )
+    flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
+        ('repo::archive',),
+    )
+
+    module.extract_last_archive_dry_run(
+        config={'extra_borg_options': {'extract': '--extra "value with space"'}},
+        local_borg_version='1.2.3',
+        global_arguments=flexmock(),
+        repository_path='repo',
+    )
+
+
 def test_extract_archive_calls_borg_with_path_flags():
 def test_extract_archive_calls_borg_with_path_flags():
     flexmock(module.os.path).should_receive('abspath').and_return('repo')
     flexmock(module.os.path).should_receive('abspath').and_return('repo')
     insert_execute_command_mock(('borg', 'extract', 'repo::archive', 'path1', 'path2'))
     insert_execute_command_mock(('borg', 'extract', 'repo::archive', 'path1', 'path2'))
@@ -368,6 +385,29 @@ def test_extract_archive_calls_borg_with_lock_wait_flags():
     )
     )
 
 
 
 
+def test_extract_archive_calls_borg_with_extra_borg_options():
+    flexmock(module.os.path).should_receive('abspath').and_return('repo')
+    insert_execute_command_mock(('borg', 'extract', '--extra', 'value with space', 'repo::archive'))
+    flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
+    flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
+        ('repo::archive',),
+    )
+    flexmock(module.borgmatic.config.validate).should_receive(
+        'normalize_repository_path',
+    ).and_return('repo')
+
+    module.extract_archive(
+        dry_run=False,
+        repository='repo',
+        archive='archive',
+        paths=None,
+        config={'extra_borg_options': {'extract': '--extra "value with space"'}},
+        local_borg_version='1.2.3',
+        global_arguments=flexmock(),
+    )
+
+
 def test_extract_archive_with_log_info_calls_borg_with_info_parameter():
 def test_extract_archive_with_log_info_calls_borg_with_info_parameter():
     flexmock(module.os.path).should_receive('abspath').and_return('repo')
     flexmock(module.os.path).should_receive('abspath').and_return('repo')
     insert_execute_command_mock(('borg', 'extract', '--info', 'repo::archive'))
     insert_execute_command_mock(('borg', 'extract', '--info', 'repo::archive'))

+ 15 - 0
tests/unit/borg/test_import_key.py

@@ -137,6 +137,21 @@ def test_import_key_calls_borg_with_lock_wait_flags():
     )
     )
 
 
 
 
+def test_import_key_calls_borg_with_extra_borg_options():
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    flexmock(module.os.path).should_receive('exists').never()
+    insert_execute_command_mock(('borg', 'key', 'import', '--extra', 'value with space', 'repo'))
+
+    module.import_key(
+        repository_path='repo',
+        config={'extra_borg_options': {'key_import': '--extra "value with space"'}},
+        local_borg_version='1.2.3',
+        import_arguments=flexmock(paper=False, path=None),
+        global_arguments=flexmock(dry_run=False),
+    )
+
+
 def test_import_key_with_log_info_calls_borg_with_info_parameter():
 def test_import_key_with_log_info_calls_borg_with_info_parameter():
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))

+ 24 - 0
tests/unit/borg/test_info.py

@@ -300,6 +300,30 @@ def test_make_info_command_with_lock_wait_passes_through_to_command():
     assert command == ('borg', 'info', '--lock-wait', '5', '--repo', 'repo')
     assert command == ('borg', 'info', '--lock-wait', '5', '--repo', 'repo')
 
 
 
 
+def test_make_info_command_with_extra_borg_options_passes_through_to_command():
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None,
+        None,
+        '2.3.4',
+    ).and_return(())
+    flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
+    config = {'extra_borg_options': {'info': '--extra "value with space"'}}
+
+    command = module.make_info_command(
+        repository_path='repo',
+        config=config,
+        local_borg_version='2.3.4',
+        global_arguments=flexmock(),
+        info_arguments=flexmock(archive=None, json=False, prefix=None, match_archives=None),
+        local_path='borg',
+        remote_path=None,
+    )
+
+    assert command == ('borg', 'info', '--extra', 'value with space', '--repo', 'repo')
+
+
 def test_make_info_command_transforms_prefix_into_match_archives_flags():
 def test_make_info_command_transforms_prefix_into_match_archives_flags():
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args(
     flexmock(module.flags).should_receive('make_flags').with_args(

+ 16 - 0
tests/unit/borg/test_list.py

@@ -129,6 +129,22 @@ def test_make_list_command_includes_lock_wait():
     assert command == ('borg', 'list', '--lock-wait', '5', 'repo')
     assert command == ('borg', 'list', '--lock-wait', '5', 'repo')
 
 
 
 
+def test_make_list_command_includes_extra_borg_options():
+    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(
+        repository_path='repo',
+        config={'extra_borg_options': {'list': '--extra "value with space"'}},
+        local_borg_version='1.2.3',
+        list_arguments=flexmock(archive=None, paths=None, json=False),
+        global_arguments=flexmock(),
+    )
+
+    assert command == ('borg', 'list', '--extra', 'value with space', 'repo')
+
+
 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').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())

+ 20 - 0
tests/unit/borg/test_mount.py

@@ -217,6 +217,26 @@ def test_mount_archive_calls_borg_with_lock_wait_flags():
     )
     )
 
 
 
 
+def test_mount_archive_calls_borg_with_extra_borg_options():
+    flexmock(module.feature).should_receive('available').and_return(False)
+    flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
+        ('repo::archive',),
+    )
+    insert_execute_command_mock(
+        ('borg', 'mount', '--extra', 'value with space', 'repo::archive', '/mnt')
+    )
+
+    mount_arguments = flexmock(mount_point='/mnt', options=None, paths=None, foreground=False)
+    module.mount_archive(
+        repository_path='repo',
+        archive='archive',
+        mount_arguments=mount_arguments,
+        config={'extra_borg_options': {'mount': '--extra "value with space"'}},
+        local_borg_version='1.2.3',
+        global_arguments=flexmock(),
+    )
+
+
 def test_mount_archive_with_log_info_calls_borg_with_info_parameter():
 def test_mount_archive_with_log_info_calls_borg_with_info_parameter():
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(

+ 36 - 0
tests/unit/borg/test_recreate.py

@@ -162,6 +162,42 @@ def test_recreate_with_lock_wait():
     )
     )
 
 
 
 
+def test_recreate_with_extra_borg_options():
+    flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
+    flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
+    flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('')
+    flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
+    flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.borg.flags).should_receive(
+        'make_repository_archive_flags',
+    ).and_return(
+        (
+            '--repo',
+            'repo',
+        ),
+    )
+    insert_execute_command_mock(
+        ('borg', 'recreate', '--extra', 'value with space', '--repo', 'repo')
+    )
+
+    module.recreate_archive(
+        repository='repo',
+        archive='archive',
+        config={'extra_borg_options': {'recreate': '--extra "value with space"'}},
+        local_borg_version='1.2.3',
+        recreate_arguments=flexmock(
+            list=None,
+            target=None,
+            comment=None,
+            timestamp=None,
+            match_archives=None,
+        ),
+        global_arguments=flexmock(dry_run=False),
+        local_path='borg',
+        patterns=None,
+    )
+
+
 def test_recreate_with_log_info():
 def test_recreate_with_log_info():
     flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
     flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
     flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
     flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)

+ 25 - 1
tests/unit/borg/test_repo_create.py

@@ -486,7 +486,7 @@ def test_create_repository_with_umask_calls_borg_with_umask_flag():
     )
     )
 
 
 
 
-def test_create_repository_with_extra_borg_options_calls_borg_with_extra_options():
+def test_create_repository_calls_borg_with_extra_borg_options():
     insert_repo_info_command_not_found_mock()
     insert_repo_info_command_not_found_mock()
     insert_repo_create_command_mock(
     insert_repo_create_command_mock(
         (*REPO_CREATE_COMMAND, '--extra', '--options', 'value with space', '--repo', 'repo'),
         (*REPO_CREATE_COMMAND, '--extra', '--options', 'value with space', '--repo', 'repo'),
@@ -509,6 +509,30 @@ def test_create_repository_with_extra_borg_options_calls_borg_with_extra_options
     )
     )
 
 
 
 
+def test_create_repository_calls_borg_with_extra_borg_options_from_deprecated_init_option():
+    flexmock(module.logger).should_receive('warning').once()
+    insert_repo_info_command_not_found_mock()
+    insert_repo_create_command_mock(
+        (*REPO_CREATE_COMMAND, '--extra', '--options', 'value with space', '--repo', 'repo'),
+    )
+    flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(
+        (
+            '--repo',
+            'repo',
+        ),
+    )
+
+    module.create_repository(
+        dry_run=False,
+        repository_path='repo',
+        config={'extra_borg_options': {'init': '--extra --options "value with space"'}},
+        local_borg_version='2.3.4',
+        global_arguments=flexmock(),
+        encryption_mode='repokey',
+    )
+
+
 def test_create_repository_calls_borg_with_working_directory():
 def test_create_repository_calls_borg_with_working_directory():
     insert_repo_info_command_not_found_mock()
     insert_repo_info_command_not_found_mock()
     insert_repo_create_command_mock(
     insert_repo_create_command_mock(

+ 21 - 0
tests/unit/borg/test_repo_delete.py

@@ -216,6 +216,27 @@ def test_make_repo_delete_command_includes_lock_wait():
     assert command == ('borg', 'repo-delete', '--lock-wait', '5', 'repo')
     assert command == ('borg', 'repo-delete', '--lock-wait', '5', 'repo')
 
 
 
 
+def test_make_repo_delete_command_includes_extra_borg_options():
+    flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
+    flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
+    flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
+        ('repo',),
+    )
+
+    command = module.make_repo_delete_command(
+        repository={'path': 'repo'},
+        config={'extra_borg_options': {'repo_delete': '--extra "value with space"'}},
+        local_borg_version='1.2.3',
+        repo_delete_arguments=flexmock(list_details=False, force=0),
+        global_arguments=flexmock(dry_run=False),
+        local_path='borg',
+        remote_path=None,
+    )
+
+    assert command == ('borg', 'repo-delete', '--extra', 'value with space', 'repo')
+
+
 def test_make_repo_delete_command_includes_list():
 def test_make_repo_delete_command_includes_list():
     flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
     flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
     flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
     flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())

+ 42 - 0
tests/unit/borg/test_repo_info.py

@@ -528,6 +528,48 @@ def test_display_repository_info_with_lock_wait_calls_borg_with_lock_wait_flags(
     )
     )
 
 
 
 
+def test_display_repository_info_calls_borg_with_extra_borg_options():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+    config = {'extra_borg_options': {'repo_info': '--extra "value with space"'}}
+    flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_flags').replace_with(
+        lambda name, value: (f'--{name}', str(value)) if value else (),
+    )
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(
+        (
+            '--repo',
+            'repo',
+        ),
+    )
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
+    flexmock(module).should_receive('execute_command_and_capture_output').with_args(
+        ('borg', 'repo-info', '--extra', 'value with space', '--json', '--repo', 'repo'),
+        environment=None,
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
+    ).and_return('[]')
+    flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'repo-info', '--extra', 'value with space', '--repo', 'repo'),
+        output_log_level=module.borgmatic.logger.ANSWER,
+        environment=None,
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
+    )
+
+    module.display_repository_info(
+        repository_path='repo',
+        config=config,
+        local_borg_version='2.3.4',
+        repo_info_arguments=flexmock(json=False),
+        global_arguments=flexmock(),
+    )
+
+
 def test_display_repository_info_calls_borg_with_working_directory():
 def test_display_repository_info_calls_borg_with_working_directory():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER

+ 63 - 0
tests/unit/borg/test_repo_list.py

@@ -412,6 +412,42 @@ def test_get_latest_archive_with_lock_wait_calls_borg_with_lock_wait_flags():
     )
     )
 
 
 
 
+def test_get_latest_archive_calls_borg_with_extra_borg_options():
+    expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
+    flexmock(module.feature).should_receive('available').and_return(False)
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
+        ('--last', '1')
+    )
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
+    flexmock(module).should_receive('execute_command_and_capture_output').with_args(
+        (
+            'borg',
+            'list',
+            *BORG_LIST_LATEST_ARGUMENTS[:-1],
+            '--extra',
+            'value with space',
+            *BORG_LIST_LATEST_ARGUMENTS[-1:],
+        ),
+        environment=None,
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
+    ).and_return(json.dumps({'archives': [expected_archive]}))
+
+    assert (
+        module.get_latest_archive(
+            'repo',
+            config={'extra_borg_options': {'repo_list': '--extra "value with space"'}},
+            local_borg_version='1.2.3',
+            global_arguments=flexmock(),
+        )
+        == expected_archive
+    )
+
+
 def test_get_latest_archive_with_consider_checkpoints_calls_borg_with_consider_checkpoints_flag():
 def test_get_latest_archive_with_consider_checkpoints_calls_borg_with_consider_checkpoints_flag():
     expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
     expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.feature).should_receive('available').and_return(False)
@@ -706,6 +742,33 @@ def test_make_repo_list_command_includes_lock_wait():
     assert command == ('borg', 'list', '--lock-wait', '5', 'repo')
     assert command == ('borg', 'list', '--lock-wait', '5', 'repo')
 
 
 
 
+def test_make_repo_list_command_includes_extra_borg_options():
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+        None,
+        None,
+        '1.2.3',
+    ).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_repo_list_command(
+        repository_path='repo',
+        config={'extra_borg_options': {'repo_list': '--extra "value with space"'}},
+        local_borg_version='1.2.3',
+        repo_list_arguments=flexmock(
+            archive=None,
+            paths=None,
+            json=False,
+            prefix=None,
+            match_archives=None,
+        ),
+        global_arguments=flexmock(),
+    )
+
+    assert command == ('borg', 'list', '--extra', 'value with space', 'repo')
+
+
 def test_make_repo_list_command_includes_local_path():
 def test_make_repo_list_command_includes_local_path():
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
     flexmock(module.flags).should_receive('make_match_archives_flags').with_args(

+ 35 - 0
tests/unit/borg/test_transfer.py

@@ -482,6 +482,41 @@ def test_transfer_archives_with_lock_wait_calls_borg_with_lock_wait_flags():
     )
     )
 
 
 
 
+def test_transfer_archives_calls_borg_with_extra_borg_options():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+    flexmock(module.flags).should_receive('make_flags').and_return(())
+    flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
+    flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
+    config = {'extra_borg_options': {'transfer': '--extra "value with space"'}}
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'transfer', '--extra', 'value with space', '--repo', 'repo'),
+        output_log_level=module.borgmatic.logger.ANSWER,
+        output_file=None,
+        environment=None,
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
+    )
+
+    module.transfer_archives(
+        dry_run=False,
+        repository_path='repo',
+        config=config,
+        local_borg_version='2.3.4',
+        transfer_arguments=flexmock(
+            archive=None,
+            progress=None,
+            match_archives=None,
+            source_repository=None,
+        ),
+        global_arguments=flexmock(),
+    )
+
+
 def test_transfer_archives_with_progress_calls_borg_with_progress_flags():
 def test_transfer_archives_with_progress_calls_borg_with_progress_flags():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER

+ 10 - 0
tests/unit/borg/test_umount.py

@@ -44,6 +44,16 @@ def test_unmount_archive_with_log_debug_calls_borg_with_debug_parameters():
     module.unmount_archive(config={}, mount_point='/mnt')
     module.unmount_archive(config={}, mount_point='/mnt')
 
 
 
 
+def test_unmount_archive_calls_borg_with_extra_borg_options():
+    insert_execute_command_mock(
+        ('borg', 'umount', '--extra', 'value with space', '/mnt'), borg_local_path='borg'
+    )
+
+    module.unmount_archive(
+        config={'extra_borg_options': {'umount': '--extra "value with space"'}}, mount_point='/mnt'
+    )
+
+
 def test_unmount_archive_calls_borg_with_local_path():
 def test_unmount_archive_calls_borg_with_local_path():
     insert_execute_command_mock(('borg1', 'umount', '/mnt'), borg_local_path='borg1')
     insert_execute_command_mock(('borg1', 'umount', '/mnt'), borg_local_path='borg1')