2
0
Эх сурвалжийг харах

Apply the "working_directory" option to all actions, not just "create". Also fix the glob expansion of "source_directories" values to respect the "working_directory" option (#609).

Dan Helfman 7 сар өмнө
parent
commit
bd4c672382
48 өөрчлөгдсөн 1608 нэмэгдсэн , 178 устгасан
  1. 4 0
      NEWS
  2. 12 9
      borgmatic/actions/check.py
  3. 2 0
      borgmatic/borg/borg.py
  4. 2 0
      borgmatic/borg/break_lock.py
  5. 2 0
      borgmatic/borg/change_passphrase.py
  6. 5 0
      borgmatic/borg/check.py
  7. 2 0
      borgmatic/borg/compact.py
  8. 28 28
      borgmatic/borg/create.py
  9. 2 0
      borgmatic/borg/delete.py
  10. 4 1
      borgmatic/borg/export_key.py
  11. 2 0
      borgmatic/borg/export_tar.py
  12. 17 7
      borgmatic/borg/extract.py
  13. 4 0
      borgmatic/borg/info.py
  14. 4 0
      borgmatic/borg/list.py
  15. 4 0
      borgmatic/borg/mount.py
  16. 2 0
      borgmatic/borg/prune.py
  17. 2 0
      borgmatic/borg/repo_create.py
  18. 2 0
      borgmatic/borg/repo_delete.py
  19. 4 0
      borgmatic/borg/repo_info.py
  20. 5 0
      borgmatic/borg/repo_list.py
  21. 3 1
      borgmatic/borg/transfer.py
  22. 5 1
      borgmatic/borg/umount.py
  23. 2 0
      borgmatic/borg/version.py
  24. 11 0
      borgmatic/config/options.py
  25. 4 3
      borgmatic/config/schema.yaml
  26. 134 8
      tests/unit/actions/test_check.py
  27. 110 28
      tests/unit/borg/test_borg.py
  28. 18 2
      tests/unit/borg/test_break_lock.py
  29. 31 2
      tests/unit/borg/test_change_passphrase.py
  30. 57 1
      tests/unit/borg/test_check.py
  31. 23 2
      tests/unit/borg/test_compact.py
  32. 214 23
      tests/unit/borg/test_create.py
  33. 42 0
      tests/unit/borg/test_delete.py
  34. 43 3
      tests/unit/borg/test_export_key.py
  35. 29 1
      tests/unit/borg/test_export_tar.py
  36. 119 6
      tests/unit/borg/test_extract.py
  37. 39 0
      tests/unit/borg/test_info.py
  38. 94 9
      tests/unit/borg/test_list.py
  39. 32 4
      tests/unit/borg/test_mount.py
  40. 36 4
      tests/unit/borg/test_prune.py
  41. 34 2
      tests/unit/borg/test_repo_create.py
  42. 43 0
      tests/unit/borg/test_repo_delete.py
  43. 110 13
      tests/unit/borg/test_repo_info.py
  44. 102 1
      tests/unit/borg/test_repo_list.py
  45. 113 16
      tests/unit/borg/test_transfer.py
  46. 16 2
      tests/unit/borg/test_umount.py
  47. 18 1
      tests/unit/borg/test_version.py
  48. 17 0
      tests/unit/config/test_options.py

+ 4 - 0
NEWS

@@ -1,4 +1,8 @@
 1.9.0.dev0
 1.9.0.dev0
+ * #609: Fix the glob expansion of "source_directories" values to respect the "working_directory"
+   option.
+ * #609: BREAKING: Apply the "working_directory" option to all actions, not just "create". This
+   includes repository paths, destination paths, mount points, etc.
  * #914: Fix a confusing apparent hang when when the repository location changes, and instead
  * #914: Fix a confusing apparent hang when when the repository location changes, and instead
    show a helpful error message.
    show a helpful error message.
  * #915: BREAKING: Rename repository actions like "rcreate" to more explicit names like
  * #915: BREAKING: Rename repository actions like "rcreate" to more explicit names like

+ 12 - 9
borgmatic/actions/check.py

@@ -14,6 +14,7 @@ import borgmatic.borg.extract
 import borgmatic.borg.list
 import borgmatic.borg.list
 import borgmatic.borg.repo_list
 import borgmatic.borg.repo_list
 import borgmatic.borg.state
 import borgmatic.borg.state
+import borgmatic.config.options
 import borgmatic.config.validate
 import borgmatic.config.validate
 import borgmatic.execute
 import borgmatic.execute
 import borgmatic.hooks.command
 import borgmatic.hooks.command
@@ -356,17 +357,13 @@ def collect_spot_check_source_paths(
         )
         )
     )
     )
     borg_environment = borgmatic.borg.environment.make_environment(config)
     borg_environment = borgmatic.borg.environment.make_environment(config)
-
-    try:
-        working_directory = os.path.expanduser(config.get('working_directory'))
-    except TypeError:
-        working_directory = None
+    working_directory = borgmatic.config.options.get_working_directory(config)
 
 
     paths_output = borgmatic.execute.execute_command_and_capture_output(
     paths_output = borgmatic.execute.execute_command_and_capture_output(
         create_flags + create_positional_arguments,
         create_flags + create_positional_arguments,
         capture_stderr=True,
         capture_stderr=True,
-        working_directory=working_directory,
         extra_environment=borg_environment,
         extra_environment=borg_environment,
+        working_directory=working_directory,
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),
     )
     )
@@ -377,7 +374,9 @@ def collect_spot_check_source_paths(
         if path_line and path_line.startswith('- ') or path_line.startswith('+ ')
         if path_line and path_line.startswith('- ') or path_line.startswith('+ ')
     )
     )
 
 
-    return tuple(path for path in paths if os.path.isfile(path))
+    return tuple(
+        path for path in paths if os.path.isfile(os.path.join(working_directory or '', path))
+    )
 
 
 
 
 BORG_DIRECTORY_FILE_TYPE = 'd'
 BORG_DIRECTORY_FILE_TYPE = 'd'
@@ -444,8 +443,11 @@ def compare_spot_check_hashes(
         int(len(source_paths) * (min(spot_check_config['data_sample_percentage'], 100) / 100)), 1
         int(len(source_paths) * (min(spot_check_config['data_sample_percentage'], 100) / 100)), 1
     )
     )
     source_sample_paths = tuple(random.sample(source_paths, sample_count))
     source_sample_paths = tuple(random.sample(source_paths, sample_count))
+    working_directory = borgmatic.config.options.get_working_directory(config)
     existing_source_sample_paths = {
     existing_source_sample_paths = {
-        source_path for source_path in source_sample_paths if os.path.exists(source_path)
+        source_path
+        for source_path in source_sample_paths
+        if os.path.exists(os.path.join(working_directory or '', source_path))
     }
     }
     logger.debug(
     logger.debug(
         f'{log_label}: Sampling {sample_count} source paths (~{spot_check_config["data_sample_percentage"]}%) for spot check'
         f'{log_label}: Sampling {sample_count} source paths (~{spot_check_config["data_sample_percentage"]}%) for spot check'
@@ -469,7 +471,8 @@ def compare_spot_check_hashes(
             (spot_check_config.get('xxh64sum_command', 'xxh64sum'),)
             (spot_check_config.get('xxh64sum_command', 'xxh64sum'),)
             + tuple(
             + tuple(
                 path for path in source_sample_paths_subset if path in existing_source_sample_paths
                 path for path in source_sample_paths_subset if path in existing_source_sample_paths
-            )
+            ),
+            working_directory=working_directory,
         )
         )
 
 
         source_hashes.update(
         source_hashes.update(

+ 2 - 0
borgmatic/borg/borg.py

@@ -2,6 +2,7 @@ import logging
 import shlex
 import shlex
 
 
 import borgmatic.commands.arguments
 import borgmatic.commands.arguments
+import borgmatic.config.options
 import borgmatic.logger
 import borgmatic.logger
 from borgmatic.borg import environment, flags
 from borgmatic.borg import environment, flags
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
@@ -67,6 +68,7 @@ def run_arbitrary_borg(
                 'ARCHIVE': archive if archive else '',
                 'ARCHIVE': archive if archive else '',
             },
             },
         ),
         ),
+        working_directory=borgmatic.config.options.get_working_directory(config),
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),
     )
     )

+ 2 - 0
borgmatic/borg/break_lock.py

@@ -1,5 +1,6 @@
 import logging
 import logging
 
 
+import borgmatic.config.options
 from borgmatic.borg import environment, flags
 from borgmatic.borg import environment, flags
 from borgmatic.execute import execute_command
 from borgmatic.execute import execute_command
 
 
@@ -37,6 +38,7 @@ def break_lock(
     execute_command(
     execute_command(
         full_command,
         full_command,
         extra_environment=borg_environment,
         extra_environment=borg_environment,
+        working_directory=borgmatic.config.options.get_working_directory(config),
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),
     )
     )

+ 2 - 0
borgmatic/borg/change_passphrase.py

@@ -1,5 +1,6 @@
 import logging
 import logging
 
 
+import borgmatic.config.options
 import borgmatic.execute
 import borgmatic.execute
 import borgmatic.logger
 import borgmatic.logger
 from borgmatic.borg import environment, flags
 from borgmatic.borg import environment, flags
@@ -56,6 +57,7 @@ def change_passphrase(
         output_file=borgmatic.execute.DO_NOT_CAPTURE,
         output_file=borgmatic.execute.DO_NOT_CAPTURE,
         output_log_level=logging.ANSWER,
         output_log_level=logging.ANSWER,
         extra_environment=environment.make_environment(config_without_passphrase),
         extra_environment=environment.make_environment(config_without_passphrase),
+        working_directory=borgmatic.config.options.get_working_directory(config),
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),
     )
     )

+ 5 - 0
borgmatic/borg/check.py

@@ -2,6 +2,7 @@ import argparse
 import json
 import json
 import logging
 import logging
 
 
+import borgmatic.config.options
 from borgmatic.borg import environment, feature, flags, repo_info
 from borgmatic.borg import environment, feature, flags, repo_info
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 
 
@@ -167,6 +168,8 @@ def check_archives(
         + flags.make_repository_flags(repository_path, local_borg_version)
         + flags.make_repository_flags(repository_path, local_borg_version)
     )
     )
 
 
+    working_directory = borgmatic.config.options.get_working_directory(config)
+
     # The Borg repair option triggers an interactive prompt, which won't work when output is
     # The Borg repair option triggers an interactive prompt, which won't work when output is
     # captured. And progress messes with the terminal directly.
     # captured. And progress messes with the terminal directly.
     if check_arguments.repair or check_arguments.progress:
     if check_arguments.repair or check_arguments.progress:
@@ -174,6 +177,7 @@ def check_archives(
             full_command,
             full_command,
             output_file=DO_NOT_CAPTURE,
             output_file=DO_NOT_CAPTURE,
             extra_environment=borg_environment,
             extra_environment=borg_environment,
+            working_directory=working_directory,
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
             borg_exit_codes=borg_exit_codes,
         )
         )
@@ -181,6 +185,7 @@ def check_archives(
         execute_command(
         execute_command(
             full_command,
             full_command,
             extra_environment=borg_environment,
             extra_environment=borg_environment,
+            working_directory=working_directory,
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
             borg_exit_codes=borg_exit_codes,
         )
         )

+ 2 - 0
borgmatic/borg/compact.py

@@ -1,5 +1,6 @@
 import logging
 import logging
 
 
+import borgmatic.config.options
 from borgmatic.borg import environment, flags
 from borgmatic.borg import environment, flags
 from borgmatic.execute import execute_command
 from borgmatic.execute import execute_command
 
 
@@ -49,6 +50,7 @@ def compact_segments(
         full_command,
         full_command,
         output_log_level=logging.INFO,
         output_log_level=logging.INFO,
         extra_environment=environment.make_environment(config),
         extra_environment=environment.make_environment(config),
+        working_directory=borgmatic.config.options.get_working_directory(config),
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),
     )
     )

+ 28 - 28
borgmatic/borg/create.py

@@ -7,6 +7,7 @@ import stat
 import tempfile
 import tempfile
 import textwrap
 import textwrap
 
 
+import borgmatic.config.options
 import borgmatic.logger
 import borgmatic.logger
 from borgmatic.borg import environment, feature, flags, state
 from borgmatic.borg import environment, feature, flags, state
 from borgmatic.execute import (
 from borgmatic.execute import (
@@ -19,26 +20,28 @@ from borgmatic.execute import (
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 
 
-def expand_directory(directory):
+def expand_directory(directory, working_directory):
     '''
     '''
     Given a directory path, expand any tilde (representing a user's home directory) and any globs
     Given a directory path, expand any tilde (representing a user's home directory) and any globs
     therein. Return a list of one or more resulting paths.
     therein. Return a list of one or more resulting paths.
     '''
     '''
-    expanded_directory = os.path.expanduser(directory)
+    expanded_directory = os.path.join(working_directory or '', os.path.expanduser(directory))
 
 
     return glob.glob(expanded_directory) or [expanded_directory]
     return glob.glob(expanded_directory) or [expanded_directory]
 
 
 
 
-def expand_directories(directories):
+def expand_directories(directories, working_directory=None):
     '''
     '''
-    Given a sequence of directory paths, expand tildes and globs in each one. Return all the
-    resulting directories as a single flattened tuple.
+    Given a sequence of directory paths and an optional working directory, expand tildes and globs
+    in each one. Return all the resulting directories as a single flattened tuple.
     '''
     '''
     if directories is None:
     if directories is None:
         return ()
         return ()
 
 
     return tuple(
     return tuple(
-        itertools.chain.from_iterable(expand_directory(directory) for directory in directories)
+        itertools.chain.from_iterable(
+            expand_directory(directory, working_directory) for directory in directories
+        )
     )
     )
 
 
 
 
@@ -53,17 +56,19 @@ def expand_home_directories(directories):
     return tuple(os.path.expanduser(directory) for directory in directories)
     return tuple(os.path.expanduser(directory) for directory in directories)
 
 
 
 
-def map_directories_to_devices(directories):
+def map_directories_to_devices(directories, working_directory=None):
     '''
     '''
-    Given a sequence of directories, return a map from directory to an identifier for the device on
-    which that directory resides or None if the path doesn't exist.
+    Given a sequence of directories and an optional working directory, return a map from directory
+    to an identifier for the device on which that directory resides or None if the path doesn't
+    exist.
 
 
     This is handy for determining whether two different directories are on the same filesystem (have
     This is handy for determining whether two different directories are on the same filesystem (have
     the same device identifier).
     the same device identifier).
     '''
     '''
     return {
     return {
-        directory: os.stat(directory).st_dev if os.path.exists(directory) else None
+        directory: os.stat(full_directory).st_dev if os.path.exists(full_directory) else None
         for directory in directories
         for directory in directories
+        for full_directory in (os.path.join(working_directory or '', directory),)
     }
     }
 
 
 
 
@@ -318,12 +323,8 @@ def check_all_source_directories_exist(source_directories, working_directory=Non
         for source_directory in source_directories
         for source_directory in source_directories
         if not all(
         if not all(
             [
             [
-                os.path.exists(directory)
-                for directory in expand_directory(
-                    os.path.join(working_directory, source_directory)
-                    if working_directory
-                    else source_directory
-                )
+                os.path.exists(os.path.join(working_directory or '', directory))
+                for directory in expand_directory(source_directory, working_directory)
             ]
             ]
         )
         )
     ]
     ]
@@ -356,10 +357,7 @@ def make_base_create_command(
     (base Borg create command flags, Borg create command positional arguments, open pattern file
     (base Borg create command flags, Borg create command positional arguments, open pattern file
     handle, open exclude file handle).
     handle, open exclude file handle).
     '''
     '''
-    try:
-        working_directory = os.path.expanduser(config.get('working_directory'))
-    except TypeError:
-        working_directory = None
+    working_directory = borgmatic.config.options.get_working_directory(config)
 
 
     if config.get('source_directories_must_exist', False):
     if config.get('source_directories_must_exist', False):
         check_all_source_directories_exist(
         check_all_source_directories_exist(
@@ -371,11 +369,15 @@ def make_base_create_command(
             expand_directories(
             expand_directories(
                 tuple(config.get('source_directories', ()))
                 tuple(config.get('source_directories', ()))
                 + borgmatic_source_directories
                 + borgmatic_source_directories
-                + tuple(config_paths if config.get('store_config_files', True) else ())
+                + tuple(config_paths if config.get('store_config_files', True) else ()),
+                working_directory=working_directory,
             )
             )
         ),
         ),
         additional_directory_devices=map_directories_to_devices(
         additional_directory_devices=map_directories_to_devices(
-            expand_directories(pattern_root_directories(config.get('patterns')))
+            expand_directories(
+                pattern_root_directories(config.get('patterns')),
+                working_directory=working_directory,
+            )
         ),
         ),
     )
     )
 
 
@@ -522,8 +524,11 @@ def create_archive(
     create command while also triggering the given processes to produce output.
     create command while also triggering the given processes to produce output.
     '''
     '''
     borgmatic.logger.add_custom_log_levels()
     borgmatic.logger.add_custom_log_levels()
+
+    working_directory = borgmatic.config.options.get_working_directory(config)
     borgmatic_source_directories = expand_directories(
     borgmatic_source_directories = expand_directories(
-        collect_borgmatic_source_directories(config.get('borgmatic_source_directory'))
+        collect_borgmatic_source_directories(config.get('borgmatic_source_directory')),
+        working_directory=working_directory,
     )
     )
 
 
     (create_flags, create_positional_arguments, pattern_file, exclude_file) = (
     (create_flags, create_positional_arguments, pattern_file, exclude_file) = (
@@ -555,11 +560,6 @@ def create_archive(
     # the terminal directly.
     # the terminal directly.
     output_file = DO_NOT_CAPTURE if progress else None
     output_file = DO_NOT_CAPTURE if progress else None
 
 
-    try:
-        working_directory = os.path.expanduser(config.get('working_directory'))
-    except TypeError:
-        working_directory = None
-
     borg_environment = environment.make_environment(config)
     borg_environment = environment.make_environment(config)
 
 
     create_flags += (
     create_flags += (

+ 2 - 0
borgmatic/borg/delete.py

@@ -5,6 +5,7 @@ import borgmatic.borg.environment
 import borgmatic.borg.feature
 import borgmatic.borg.feature
 import borgmatic.borg.flags
 import borgmatic.borg.flags
 import borgmatic.borg.repo_delete
 import borgmatic.borg.repo_delete
+import borgmatic.config.options
 import borgmatic.execute
 import borgmatic.execute
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
@@ -127,6 +128,7 @@ def delete_archives(
         command,
         command,
         output_log_level=logging.ANSWER,
         output_log_level=logging.ANSWER,
         extra_environment=borgmatic.borg.environment.make_environment(config),
         extra_environment=borgmatic.borg.environment.make_environment(config),
+        working_directory=borgmatic.config.options.get_working_directory(config),
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),
     )
     )

+ 4 - 1
borgmatic/borg/export_key.py

@@ -1,6 +1,7 @@
 import logging
 import logging
 import os
 import os
 
 
+import borgmatic.config.options
 import borgmatic.logger
 import borgmatic.logger
 from borgmatic.borg import environment, flags
 from borgmatic.borg import environment, flags
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
@@ -29,9 +30,10 @@ def export_key(
     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)
+    working_directory = borgmatic.config.options.get_working_directory(config)
 
 
     if export_arguments.path and export_arguments.path != '-':
     if export_arguments.path and export_arguments.path != '-':
-        if os.path.exists(export_arguments.path):
+        if os.path.exists(os.path.join(working_directory or '', export_arguments.path)):
             raise FileExistsError(
             raise FileExistsError(
                 f'Destination path {export_arguments.path} already exists. Aborting.'
                 f'Destination path {export_arguments.path} already exists. Aborting.'
             )
             )
@@ -66,6 +68,7 @@ def export_key(
         output_file=output_file,
         output_file=output_file,
         output_log_level=logging.ANSWER,
         output_log_level=logging.ANSWER,
         extra_environment=environment.make_environment(config),
         extra_environment=environment.make_environment(config),
+        working_directory=working_directory,
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),
     )
     )

+ 2 - 0
borgmatic/borg/export_tar.py

@@ -1,5 +1,6 @@
 import logging
 import logging
 
 
+import borgmatic.config.options
 import borgmatic.logger
 import borgmatic.logger
 from borgmatic.borg import environment, flags
 from borgmatic.borg import environment, flags
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
@@ -70,6 +71,7 @@ def export_tar_archive(
         output_file=DO_NOT_CAPTURE if destination_path == '-' else None,
         output_file=DO_NOT_CAPTURE if destination_path == '-' else None,
         output_log_level=output_log_level,
         output_log_level=output_log_level,
         extra_environment=environment.make_environment(config),
         extra_environment=environment.make_environment(config),
+        working_directory=borgmatic.config.options.get_working_directory(config),
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),
     )
     )

+ 17 - 7
borgmatic/borg/extract.py

@@ -2,6 +2,7 @@ import logging
 import os
 import os
 import subprocess
 import subprocess
 
 
+import borgmatic.config.options
 import borgmatic.config.validate
 import borgmatic.config.validate
 from borgmatic.borg import environment, feature, flags, repo_list
 from borgmatic.borg import environment, feature, flags, repo_list
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
@@ -58,8 +59,8 @@ def extract_last_archive_dry_run(
 
 
     execute_command(
     execute_command(
         full_extract_command,
         full_extract_command,
-        working_directory=None,
         extra_environment=borg_environment,
         extra_environment=borg_environment,
+        working_directory=borgmatic.config.options.get_working_directory(config),
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),
     )
     )
@@ -112,6 +113,8 @@ def extract_archive(
             *(len(tuple(piece for piece in path.split(os.path.sep) if piece)) - 1 for path in paths)
             *(len(tuple(piece for piece in path.split(os.path.sep) if piece)) - 1 for path in paths)
         )
         )
 
 
+    working_directory = borgmatic.config.options.get_working_directory(config)
+
     full_command = (
     full_command = (
         (local_path, 'extract')
         (local_path, 'extract')
         + (('--remote-path', remote_path) if remote_path else ())
         + (('--remote-path', remote_path) if remote_path else ())
@@ -126,9 +129,13 @@ def extract_archive(
         + (('--progress',) if progress else ())
         + (('--progress',) if progress else ())
         + (('--stdout',) if extract_to_stdout else ())
         + (('--stdout',) if extract_to_stdout else ())
         + flags.make_repository_archive_flags(
         + flags.make_repository_archive_flags(
-            # Make the repository path absolute so the working directory changes below don't
-            # prevent Borg from finding the repo.
-            borgmatic.config.validate.normalize_repository_path(repository),
+            # 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 user's configured
+            # working directory (if any) to the repo path.
+            borgmatic.config.validate.normalize_repository_path(
+                os.path.join(working_directory or '', repository)
+            ),
             archive,
             archive,
             local_borg_version,
             local_borg_version,
         )
         )
@@ -137,6 +144,9 @@ def extract_archive(
 
 
     borg_environment = environment.make_environment(config)
     borg_environment = environment.make_environment(config)
     borg_exit_codes = config.get('borg_exit_codes')
     borg_exit_codes = config.get('borg_exit_codes')
+    full_destination_path = (
+        os.path.join(working_directory or '', destination_path) if destination_path else None
+    )
 
 
     # The progress output isn't compatible with captured and logged output, as progress messes with
     # The progress output isn't compatible with captured and logged output, as progress messes with
     # the terminal directly.
     # the terminal directly.
@@ -144,8 +154,8 @@ def extract_archive(
         return execute_command(
         return execute_command(
             full_command,
             full_command,
             output_file=DO_NOT_CAPTURE,
             output_file=DO_NOT_CAPTURE,
-            working_directory=destination_path,
             extra_environment=borg_environment,
             extra_environment=borg_environment,
+            working_directory=full_destination_path,
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
             borg_exit_codes=borg_exit_codes,
         )
         )
@@ -155,9 +165,9 @@ def extract_archive(
         return execute_command(
         return execute_command(
             full_command,
             full_command,
             output_file=subprocess.PIPE,
             output_file=subprocess.PIPE,
-            working_directory=destination_path,
             run_to_completion=False,
             run_to_completion=False,
             extra_environment=borg_environment,
             extra_environment=borg_environment,
+            working_directory=full_destination_path,
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
             borg_exit_codes=borg_exit_codes,
         )
         )
@@ -166,8 +176,8 @@ def extract_archive(
     # if the restore paths don't exist in the archive.
     # if the restore paths don't exist in the archive.
     execute_command(
     execute_command(
         full_command,
         full_command,
-        working_directory=destination_path,
         extra_environment=borg_environment,
         extra_environment=borg_environment,
+        working_directory=full_destination_path,
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
     )
     )

+ 4 - 0
borgmatic/borg/info.py

@@ -1,6 +1,7 @@
 import argparse
 import argparse
 import logging
 import logging
 
 
+import borgmatic.config.options
 import borgmatic.logger
 import borgmatic.logger
 from borgmatic.borg import environment, feature, flags
 from borgmatic.borg import environment, feature, flags
 from borgmatic.execute import execute_command, execute_command_and_capture_output
 from borgmatic.execute import execute_command, execute_command_and_capture_output
@@ -96,10 +97,12 @@ def display_archives_info(
         remote_path,
         remote_path,
     )
     )
     borg_exit_codes = config.get('borg_exit_codes')
     borg_exit_codes = config.get('borg_exit_codes')
+    working_directory = borgmatic.config.options.get_working_directory(config)
 
 
     json_info = execute_command_and_capture_output(
     json_info = execute_command_and_capture_output(
         json_command,
         json_command,
         extra_environment=environment.make_environment(config),
         extra_environment=environment.make_environment(config),
+        working_directory=working_directory,
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
     )
     )
@@ -113,6 +116,7 @@ def display_archives_info(
         main_command,
         main_command,
         output_log_level=logging.ANSWER,
         output_log_level=logging.ANSWER,
         extra_environment=environment.make_environment(config),
         extra_environment=environment.make_environment(config),
+        working_directory=working_directory,
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
     )
     )

+ 4 - 0
borgmatic/borg/list.py

@@ -3,6 +3,7 @@ import copy
 import logging
 import logging
 import re
 import re
 
 
+import borgmatic.config.options
 import borgmatic.logger
 import borgmatic.logger
 from borgmatic.borg import environment, feature, flags, repo_list
 from borgmatic.borg import environment, feature, flags, repo_list
 from borgmatic.execute import execute_command, execute_command_and_capture_output
 from borgmatic.execute import execute_command, execute_command_and_capture_output
@@ -127,6 +128,7 @@ def capture_archive_listing(
                 remote_path,
                 remote_path,
             ),
             ),
             extra_environment=borg_environment,
             extra_environment=borg_environment,
+            working_directory=borgmatic.config.options.get_working_directory(config),
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=config.get('borg_exit_codes'),
             borg_exit_codes=config.get('borg_exit_codes'),
         )
         )
@@ -224,6 +226,7 @@ def list_archive(
                     remote_path,
                     remote_path,
                 ),
                 ),
                 extra_environment=borg_environment,
                 extra_environment=borg_environment,
+                working_directory=borgmatic.config.options.get_working_directory(config),
                 borg_local_path=local_path,
                 borg_local_path=local_path,
                 borg_exit_codes=borg_exit_codes,
                 borg_exit_codes=borg_exit_codes,
             )
             )
@@ -259,6 +262,7 @@ def list_archive(
             main_command,
             main_command,
             output_log_level=logging.ANSWER,
             output_log_level=logging.ANSWER,
             extra_environment=borg_environment,
             extra_environment=borg_environment,
+            working_directory=borgmatic.config.options.get_working_directory(config),
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
             borg_exit_codes=borg_exit_codes,
         )
         )

+ 4 - 0
borgmatic/borg/mount.py

@@ -1,5 +1,6 @@
 import logging
 import logging
 
 
+import borgmatic.config.options
 from borgmatic.borg import environment, feature, flags
 from borgmatic.borg import environment, feature, flags
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 
 
@@ -59,6 +60,7 @@ def mount_archive(
     )
     )
 
 
     borg_environment = environment.make_environment(config)
     borg_environment = environment.make_environment(config)
+    working_directory = borgmatic.config.options.get_working_directory(config)
 
 
     # Don't capture the output when foreground mode is used so that ctrl-C can work properly.
     # Don't capture the output when foreground mode is used so that ctrl-C can work properly.
     if mount_arguments.foreground:
     if mount_arguments.foreground:
@@ -66,6 +68,7 @@ def mount_archive(
             full_command,
             full_command,
             output_file=DO_NOT_CAPTURE,
             output_file=DO_NOT_CAPTURE,
             extra_environment=borg_environment,
             extra_environment=borg_environment,
+            working_directory=working_directory,
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=config.get('borg_exit_codes'),
             borg_exit_codes=config.get('borg_exit_codes'),
         )
         )
@@ -74,6 +77,7 @@ def mount_archive(
     execute_command(
     execute_command(
         full_command,
         full_command,
         extra_environment=borg_environment,
         extra_environment=borg_environment,
+        working_directory=working_directory,
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),
     )
     )

+ 2 - 0
borgmatic/borg/prune.py

@@ -1,5 +1,6 @@
 import logging
 import logging
 
 
+import borgmatic.config.options
 import borgmatic.logger
 import borgmatic.logger
 from borgmatic.borg import environment, feature, flags
 from borgmatic.borg import environment, feature, flags
 from borgmatic.execute import execute_command
 from borgmatic.execute import execute_command
@@ -95,6 +96,7 @@ def prune_archives(
         full_command,
         full_command,
         output_log_level=output_log_level,
         output_log_level=output_log_level,
         extra_environment=environment.make_environment(config),
         extra_environment=environment.make_environment(config),
+        working_directory=borgmatic.config.options.get_working_directory(config),
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),
     )
     )

+ 2 - 0
borgmatic/borg/repo_create.py

@@ -3,6 +3,7 @@ import json
 import logging
 import logging
 import subprocess
 import subprocess
 
 
+import borgmatic.config.options
 from borgmatic.borg import environment, feature, flags, repo_info
 from borgmatic.borg import environment, feature, flags, repo_info
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 
 
@@ -96,6 +97,7 @@ def create_repository(
         repo_create_command,
         repo_create_command,
         output_file=DO_NOT_CAPTURE,
         output_file=DO_NOT_CAPTURE,
         extra_environment=environment.make_environment(config),
         extra_environment=environment.make_environment(config),
+        working_directory=borgmatic.config.options.get_working_directory(config),
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),
     )
     )

+ 2 - 0
borgmatic/borg/repo_delete.py

@@ -3,6 +3,7 @@ import logging
 import borgmatic.borg.environment
 import borgmatic.borg.environment
 import borgmatic.borg.feature
 import borgmatic.borg.feature
 import borgmatic.borg.flags
 import borgmatic.borg.flags
+import borgmatic.config.options
 import borgmatic.execute
 import borgmatic.execute
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
@@ -87,6 +88,7 @@ def delete_repository(
             else borgmatic.execute.DO_NOT_CAPTURE
             else borgmatic.execute.DO_NOT_CAPTURE
         ),
         ),
         extra_environment=borgmatic.borg.environment.make_environment(config),
         extra_environment=borgmatic.borg.environment.make_environment(config),
+        working_directory=borgmatic.config.options.get_working_directory(config),
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),
     )
     )

+ 4 - 0
borgmatic/borg/repo_info.py

@@ -1,5 +1,6 @@
 import logging
 import logging
 
 
+import borgmatic.config.options
 import borgmatic.logger
 import borgmatic.logger
 from borgmatic.borg import environment, feature, flags
 from borgmatic.borg import environment, feature, flags
 from borgmatic.execute import execute_command, execute_command_and_capture_output
 from borgmatic.execute import execute_command, execute_command_and_capture_output
@@ -49,12 +50,14 @@ def display_repository_info(
     )
     )
 
 
     extra_environment = environment.make_environment(config)
     extra_environment = environment.make_environment(config)
+    working_directory = borgmatic.config.options.get_working_directory(config)
     borg_exit_codes = config.get('borg_exit_codes')
     borg_exit_codes = config.get('borg_exit_codes')
 
 
     if repo_info_arguments.json:
     if repo_info_arguments.json:
         return execute_command_and_capture_output(
         return execute_command_and_capture_output(
             full_command,
             full_command,
             extra_environment=extra_environment,
             extra_environment=extra_environment,
+            working_directory=working_directory,
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
             borg_exit_codes=borg_exit_codes,
         )
         )
@@ -63,6 +66,7 @@ def display_repository_info(
             full_command,
             full_command,
             output_log_level=logging.ANSWER,
             output_log_level=logging.ANSWER,
             extra_environment=extra_environment,
             extra_environment=extra_environment,
+            working_directory=working_directory,
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
             borg_exit_codes=borg_exit_codes,
         )
         )

+ 5 - 0
borgmatic/borg/repo_list.py

@@ -1,6 +1,7 @@
 import argparse
 import argparse
 import logging
 import logging
 
 
+import borgmatic.config.options
 import borgmatic.logger
 import borgmatic.logger
 from borgmatic.borg import environment, feature, flags
 from borgmatic.borg import environment, feature, flags
 from borgmatic.execute import execute_command, execute_command_and_capture_output
 from borgmatic.execute import execute_command, execute_command_and_capture_output
@@ -48,6 +49,7 @@ def resolve_archive_name(
     output = execute_command_and_capture_output(
     output = execute_command_and_capture_output(
         full_command,
         full_command,
         extra_environment=environment.make_environment(config),
         extra_environment=environment.make_environment(config),
+        working_directory=borgmatic.config.options.get_working_directory(config),
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),
     )
     )
@@ -156,11 +158,13 @@ def list_repository(
         local_path,
         local_path,
         remote_path,
         remote_path,
     )
     )
+    working_directory = borgmatic.config.options.get_working_directory(config)
     borg_exit_codes = config.get('borg_exit_codes')
     borg_exit_codes = config.get('borg_exit_codes')
 
 
     json_listing = execute_command_and_capture_output(
     json_listing = execute_command_and_capture_output(
         json_command,
         json_command,
         extra_environment=borg_environment,
         extra_environment=borg_environment,
+        working_directory=working_directory,
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
     )
     )
@@ -174,6 +178,7 @@ def list_repository(
         main_command,
         main_command,
         output_log_level=logging.ANSWER,
         output_log_level=logging.ANSWER,
         extra_environment=borg_environment,
         extra_environment=borg_environment,
+        working_directory=working_directory,
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
     )
     )

+ 3 - 1
borgmatic/borg/transfer.py

@@ -1,5 +1,6 @@
 import logging
 import logging
 
 
+import borgmatic.config.options
 import borgmatic.logger
 import borgmatic.logger
 from borgmatic.borg import environment, flags
 from borgmatic.borg import environment, flags
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
@@ -55,7 +56,8 @@ def transfer_archives(
         full_command,
         full_command,
         output_log_level=logging.ANSWER,
         output_log_level=logging.ANSWER,
         output_file=DO_NOT_CAPTURE if transfer_arguments.progress else None,
         output_file=DO_NOT_CAPTURE if transfer_arguments.progress else None,
+        extra_environment=environment.make_environment(config),
+        working_directory=borgmatic.config.options.get_working_directory(config),
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),
-        extra_environment=environment.make_environment(config),
     )
     )

+ 5 - 1
borgmatic/borg/umount.py

@@ -1,5 +1,6 @@
 import logging
 import logging
 
 
+import borgmatic.config.options
 from borgmatic.execute import execute_command
 from borgmatic.execute import execute_command
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
@@ -18,5 +19,8 @@ def unmount_archive(config, mount_point, local_path='borg'):
     )
     )
 
 
     execute_command(
     execute_command(
-        full_command, borg_local_path=local_path, borg_exit_codes=config.get('borg_exit_codes')
+        full_command,
+        working_directory=borgmatic.config.options.get_working_directory(config),
+        borg_local_path=local_path,
+        borg_exit_codes=config.get('borg_exit_codes'),
     )
     )

+ 2 - 0
borgmatic/borg/version.py

@@ -1,5 +1,6 @@
 import logging
 import logging
 
 
+import borgmatic.config.options
 from borgmatic.borg import environment
 from borgmatic.borg import environment
 from borgmatic.execute import execute_command_and_capture_output
 from borgmatic.execute import execute_command_and_capture_output
 
 
@@ -21,6 +22,7 @@ def local_borg_version(config, local_path='borg'):
     output = execute_command_and_capture_output(
     output = execute_command_and_capture_output(
         full_command,
         full_command,
         extra_environment=environment.make_environment(config),
         extra_environment=environment.make_environment(config),
+        working_directory=borgmatic.config.options.get_working_directory(config),
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),
     )
     )

+ 11 - 0
borgmatic/config/options.py

@@ -0,0 +1,11 @@
+import os
+
+
+def get_working_directory(config):
+    '''
+    Given a configuration dict, get the working directory from it, first expanding any tildes.
+    '''
+    try:
+        return os.path.expanduser(config.get('working_directory', '')) or None
+    except TypeError:
+        return None

+ 4 - 3
borgmatic/config/schema.yaml

@@ -58,9 +58,10 @@ properties:
     working_directory:
     working_directory:
         type: string
         type: string
         description: |
         description: |
-            Working directory for the "borg create" command. Tildes are
-            expanded. Useful for backing up using relative paths. See
-            http://borgbackup.readthedocs.io/en/stable/usage/create.html for
+            Working directory to use when running actions, useful for backing up
+            using relative source directory paths. Does not currently apply to
+            borgmatic configuration file paths or includes. Tildes are expanded.
+            See http://borgbackup.readthedocs.io/en/stable/usage/create.html for
             details. Defaults to not set.
             details. Defaults to not set.
         example: /path/to/working/directory
         example: /path/to/working/directory
     one_file_system:
     one_file_system:

+ 134 - 8
tests/unit/actions/test_check.py

@@ -497,6 +497,9 @@ def test_collect_spot_check_source_paths_parses_borg_output():
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
         flexmock()
         flexmock()
     )
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive(
     flexmock(module.borgmatic.execute).should_receive(
         'execute_command_and_capture_output'
         'execute_command_and_capture_output'
     ).and_return(
     ).and_return(
@@ -534,6 +537,9 @@ def test_collect_spot_check_source_paths_passes_through_stream_processes_false()
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
         flexmock()
         flexmock()
     )
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive(
     flexmock(module.borgmatic.execute).should_receive(
         'execute_command_and_capture_output'
         'execute_command_and_capture_output'
     ).and_return(
     ).and_return(
@@ -571,6 +577,9 @@ def test_collect_spot_check_source_paths_without_working_directory_parses_borg_o
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
         flexmock()
         flexmock()
     )
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive(
     flexmock(module.borgmatic.execute).should_receive(
         'execute_command_and_capture_output'
         'execute_command_and_capture_output'
     ).and_return(
     ).and_return(
@@ -608,6 +617,9 @@ def test_collect_spot_check_source_paths_skips_directories():
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
         flexmock()
         flexmock()
     )
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive(
     flexmock(module.borgmatic.execute).should_receive(
         'execute_command_and_capture_output'
         'execute_command_and_capture_output'
     ).and_return(
     ).and_return(
@@ -668,14 +680,60 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_source_dire
     ) == ('/etc/path',)
     ) == ('/etc/path',)
 
 
 
 
+def test_collect_spot_check_source_paths_uses_working_directory():
+    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return(
+        {'hook1': False, 'hook2': True}
+    )
+    flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args(
+        dry_run=True,
+        repository_path='repo',
+        config=object,
+        config_paths=(),
+        local_borg_version=object,
+        global_arguments=object,
+        borgmatic_source_directories=(),
+        local_path=object,
+        remote_path=object,
+        list_files=True,
+        stream_processes=True,
+    ).and_return((('borg', 'create'), ('repo::archive',), flexmock(), flexmock()))
+    flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
+        flexmock()
+    )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        '/working/dir'
+    )
+    flexmock(module.borgmatic.execute).should_receive(
+        'execute_command_and_capture_output'
+    ).and_return(
+        'warning: stuff\n- foo\n+ bar\n? /nope',
+    )
+    flexmock(module.os.path).should_receive('isfile').with_args('/working/dir/foo').and_return(True)
+    flexmock(module.os.path).should_receive('isfile').with_args('/working/dir/bar').and_return(True)
+
+    assert module.collect_spot_check_source_paths(
+        repository={'path': 'repo'},
+        config={'working_directory': '/working/dir'},
+        local_borg_version=flexmock(),
+        global_arguments=flexmock(),
+        local_path=flexmock(),
+        remote_path=flexmock(),
+    ) == ('foo', 'bar')
+
+
 def test_compare_spot_check_hashes_returns_paths_having_failing_hashes():
 def test_compare_spot_check_hashes_returns_paths_having_failing_hashes():
     flexmock(module.random).should_receive('sample').replace_with(
     flexmock(module.random).should_receive('sample').replace_with(
         lambda population, count: population[:count]
         lambda population, count: population[:count]
     )
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None,
+    )
     flexmock(module.os.path).should_receive('exists').and_return(True)
     flexmock(module.os.path).should_receive('exists').and_return(True)
     flexmock(module.borgmatic.execute).should_receive(
     flexmock(module.borgmatic.execute).should_receive(
         'execute_command_and_capture_output'
         'execute_command_and_capture_output'
-    ).with_args(('xxh64sum', '/foo', '/bar')).and_return('hash1  /foo\nhash2  /bar')
+    ).with_args(('xxh64sum', '/foo', '/bar'), working_directory=None).and_return(
+        'hash1  /foo\nhash2  /bar'
+    )
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
         ['hash1 /foo', 'nothash2 /bar']
         ['hash1 /foo', 'nothash2 /bar']
     )
     )
@@ -708,10 +766,15 @@ def test_compare_spot_check_hashes_handles_data_sample_percentage_above_100():
     flexmock(module.random).should_receive('sample').replace_with(
     flexmock(module.random).should_receive('sample').replace_with(
         lambda population, count: population[:count]
         lambda population, count: population[:count]
     )
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None,
+    )
     flexmock(module.os.path).should_receive('exists').and_return(True)
     flexmock(module.os.path).should_receive('exists').and_return(True)
     flexmock(module.borgmatic.execute).should_receive(
     flexmock(module.borgmatic.execute).should_receive(
         'execute_command_and_capture_output'
         'execute_command_and_capture_output'
-    ).with_args(('xxh64sum', '/foo', '/bar')).and_return('hash1  /foo\nhash2  /bar')
+    ).with_args(('xxh64sum', '/foo', '/bar'), working_directory=None).and_return(
+        'hash1  /foo\nhash2  /bar'
+    )
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
         ['nothash1 /foo', 'nothash2 /bar']
         ['nothash1 /foo', 'nothash2 /bar']
     )
     )
@@ -744,10 +807,15 @@ def test_compare_spot_check_hashes_uses_xxh64sum_command_option():
     flexmock(module.random).should_receive('sample').replace_with(
     flexmock(module.random).should_receive('sample').replace_with(
         lambda population, count: population[:count]
         lambda population, count: population[:count]
     )
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None,
+    )
     flexmock(module.os.path).should_receive('exists').and_return(True)
     flexmock(module.os.path).should_receive('exists').and_return(True)
     flexmock(module.borgmatic.execute).should_receive(
     flexmock(module.borgmatic.execute).should_receive(
         'execute_command_and_capture_output'
         'execute_command_and_capture_output'
-    ).with_args(('/usr/local/bin/xxh64sum', '/foo', '/bar')).and_return('hash1  /foo\nhash2  /bar')
+    ).with_args(('/usr/local/bin/xxh64sum', '/foo', '/bar'), working_directory=None).and_return(
+        'hash1  /foo\nhash2  /bar'
+    )
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
         ['hash1 /foo', 'nothash2 /bar']
         ['hash1 /foo', 'nothash2 /bar']
     )
     )
@@ -773,14 +841,19 @@ def test_compare_spot_check_hashes_uses_xxh64sum_command_option():
     ) == ('/bar',)
     ) == ('/bar',)
 
 
 
 
-def test_compare_spot_check_hashes_consider_path_missing_from_archive_as_not_matching():
+def test_compare_spot_check_hashes_considers_path_missing_from_archive_as_not_matching():
     flexmock(module.random).should_receive('sample').replace_with(
     flexmock(module.random).should_receive('sample').replace_with(
         lambda population, count: population[:count]
         lambda population, count: population[:count]
     )
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None,
+    )
     flexmock(module.os.path).should_receive('exists').and_return(True)
     flexmock(module.os.path).should_receive('exists').and_return(True)
     flexmock(module.borgmatic.execute).should_receive(
     flexmock(module.borgmatic.execute).should_receive(
         'execute_command_and_capture_output'
         'execute_command_and_capture_output'
-    ).with_args(('xxh64sum', '/foo', '/bar')).and_return('hash1  /foo\nhash2  /bar')
+    ).with_args(('xxh64sum', '/foo', '/bar'), working_directory=None).and_return(
+        'hash1  /foo\nhash2  /bar'
+    )
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
         ['hash1 /foo']
         ['hash1 /foo']
     )
     )
@@ -809,11 +882,14 @@ def test_compare_spot_check_hashes_considers_non_existent_path_as_not_matching()
     flexmock(module.random).should_receive('sample').replace_with(
     flexmock(module.random).should_receive('sample').replace_with(
         lambda population, count: population[:count]
         lambda population, count: population[:count]
     )
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None,
+    )
     flexmock(module.os.path).should_receive('exists').with_args('/foo').and_return(True)
     flexmock(module.os.path).should_receive('exists').with_args('/foo').and_return(True)
     flexmock(module.os.path).should_receive('exists').with_args('/bar').and_return(False)
     flexmock(module.os.path).should_receive('exists').with_args('/bar').and_return(False)
     flexmock(module.borgmatic.execute).should_receive(
     flexmock(module.borgmatic.execute).should_receive(
         'execute_command_and_capture_output'
         'execute_command_and_capture_output'
-    ).with_args(('xxh64sum', '/foo')).and_return('hash1  /foo')
+    ).with_args(('xxh64sum', '/foo'), working_directory=None).and_return('hash1  /foo')
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
         ['hash1 /foo', 'hash2 /bar']
         ['hash1 /foo', 'hash2 /bar']
     )
     )
@@ -843,13 +919,20 @@ def test_compare_spot_check_hashes_with_too_many_paths_feeds_them_to_commands_in
     flexmock(module.random).should_receive('sample').replace_with(
     flexmock(module.random).should_receive('sample').replace_with(
         lambda population, count: population[:count]
         lambda population, count: population[:count]
     )
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None,
+    )
     flexmock(module.os.path).should_receive('exists').and_return(True)
     flexmock(module.os.path).should_receive('exists').and_return(True)
     flexmock(module.borgmatic.execute).should_receive(
     flexmock(module.borgmatic.execute).should_receive(
         'execute_command_and_capture_output'
         'execute_command_and_capture_output'
-    ).with_args(('xxh64sum', '/foo', '/bar')).and_return('hash1  /foo\nhash2  /bar')
+    ).with_args(('xxh64sum', '/foo', '/bar'), working_directory=None).and_return(
+        'hash1  /foo\nhash2  /bar'
+    )
     flexmock(module.borgmatic.execute).should_receive(
     flexmock(module.borgmatic.execute).should_receive(
         'execute_command_and_capture_output'
         'execute_command_and_capture_output'
-    ).with_args(('xxh64sum', '/baz', '/quux')).and_return('hash3  /baz\nhash4  /quux')
+    ).with_args(('xxh64sum', '/baz', '/quux'), working_directory=None).and_return(
+        'hash3  /baz\nhash4  /quux'
+    )
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
         ['hash1 /foo', 'hash2 /bar']
         ['hash1 /foo', 'hash2 /bar']
     ).and_return(['hash3 /baz', 'nothash4 /quux'])
     ).and_return(['hash3 /baz', 'nothash4 /quux'])
@@ -878,6 +961,49 @@ def test_compare_spot_check_hashes_with_too_many_paths_feeds_them_to_commands_in
     ) == ('/quux',)
     ) == ('/quux',)
 
 
 
 
+def test_compare_spot_check_hashes_uses_working_directory_to_access_source_paths():
+    flexmock(module.random).should_receive('sample').replace_with(
+        lambda population, count: population[:count]
+    )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        '/working/dir',
+    )
+    flexmock(module.os.path).should_receive('exists').with_args('/working/dir/foo').and_return(True)
+    flexmock(module.os.path).should_receive('exists').with_args('/working/dir/bar').and_return(True)
+    flexmock(module.borgmatic.execute).should_receive(
+        'execute_command_and_capture_output'
+    ).with_args(('xxh64sum', 'foo', 'bar'), working_directory='/working/dir').and_return(
+        'hash1  foo\nhash2  bar'
+    )
+    flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
+        ['hash1 foo', 'nothash2 bar']
+    )
+
+    assert module.compare_spot_check_hashes(
+        repository={'path': 'repo'},
+        archive='archive',
+        config={
+            'checks': [
+                {
+                    'name': 'archives',
+                    'frequency': '2 weeks',
+                },
+                {
+                    'name': 'spot',
+                    'data_sample_percentage': 50,
+                },
+            ],
+            'working_directory': '/working/dir',
+        },
+        local_borg_version=flexmock(),
+        global_arguments=flexmock(),
+        local_path=flexmock(),
+        remote_path=flexmock(),
+        log_label='repo',
+        source_paths=('foo', 'bar', 'baz', 'quux'),
+    ) == ('bar',)
+
+
 def test_spot_check_without_spot_configuration_errors():
 def test_spot_check_without_spot_configuration_errors():
     with pytest.raises(ValueError):
     with pytest.raises(ValueError):
         module.spot_check(
         module.spot_check(

+ 110 - 28
tests/unit/borg/test_borg.py

@@ -12,13 +12,17 @@ def test_run_arbitrary_borg_calls_borg_with_flags():
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', '::'),
         ('borg', 'break-lock', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
     )
 
 
     module.run_arbitrary_borg(
     module.run_arbitrary_borg(
@@ -34,13 +38,17 @@ def test_run_arbitrary_borg_with_log_info_calls_borg_with_info_flag():
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', '--info', '::'),
         ('borg', 'break-lock', '--info', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
     )
     insert_logging_mock(logging.INFO)
     insert_logging_mock(logging.INFO)
 
 
@@ -57,13 +65,17 @@ def test_run_arbitrary_borg_with_log_debug_calls_borg_with_debug_flag():
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', '--debug', '--show-rc', '::'),
         ('borg', 'break-lock', '--debug', '--show-rc', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
     )
     insert_logging_mock(logging.DEBUG)
     insert_logging_mock(logging.DEBUG)
 
 
@@ -83,13 +95,17 @@ def test_run_arbitrary_borg_with_lock_wait_calls_borg_with_lock_wait_flags():
         ('--lock-wait', '5')
         ('--lock-wait', '5')
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', '--lock-wait', '5', '::'),
         ('borg', 'break-lock', '--lock-wait', '5', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
     )
 
 
     module.run_arbitrary_borg(
     module.run_arbitrary_borg(
@@ -105,13 +121,17 @@ def test_run_arbitrary_borg_with_archive_calls_borg_with_archive_flag():
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', "'::$ARCHIVE'"),
         ('borg', 'break-lock', "'::$ARCHIVE'"),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': 'archive'},
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': 'archive'},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
     )
 
 
     module.run_arbitrary_borg(
     module.run_arbitrary_borg(
@@ -128,13 +148,17 @@ def test_run_arbitrary_borg_with_local_path_calls_borg_via_local_path():
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg1', 'break-lock', '::'),
         ('borg1', 'break-lock', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg1',
-        borg_exit_codes=None,
         shell=True,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg1',
+        borg_exit_codes=None,
     )
     )
 
 
     module.run_arbitrary_borg(
     module.run_arbitrary_borg(
@@ -152,13 +176,17 @@ def test_run_arbitrary_borg_with_exit_codes_calls_borg_using_them():
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
     borg_exit_codes = flexmock()
     borg_exit_codes = flexmock()
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', '::'),
         ('borg', 'break-lock', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=borg_exit_codes,
         shell=True,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=borg_exit_codes,
     )
     )
 
 
     module.run_arbitrary_borg(
     module.run_arbitrary_borg(
@@ -176,13 +204,17 @@ def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_flags()
         ('--remote-path', 'borg1')
         ('--remote-path', 'borg1')
     ).and_return(())
     ).and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', '--remote-path', 'borg1', '::'),
         ('borg', 'break-lock', '--remote-path', 'borg1', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
     )
 
 
     module.run_arbitrary_borg(
     module.run_arbitrary_borg(
@@ -201,13 +233,17 @@ def test_run_arbitrary_borg_with_remote_path_injection_attack_gets_escaped():
         ('--remote-path', 'borg1; naughty-command')
         ('--remote-path', 'borg1; naughty-command')
     ).and_return(())
     ).and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', '--remote-path', "'borg1; naughty-command'", '::'),
         ('borg', 'break-lock', '--remote-path', "'borg1; naughty-command'", '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
     )
 
 
     module.run_arbitrary_borg(
     module.run_arbitrary_borg(
@@ -224,13 +260,17 @@ def test_run_arbitrary_borg_passes_borg_specific_flags_to_borg():
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', '--progress', '::'),
         ('borg', 'list', '--progress', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
     )
 
 
     module.run_arbitrary_borg(
     module.run_arbitrary_borg(
@@ -246,13 +286,17 @@ def test_run_arbitrary_borg_omits_dash_dash_in_flags_passed_to_borg():
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', '::'),
         ('borg', 'break-lock', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
     )
 
 
     module.run_arbitrary_borg(
     module.run_arbitrary_borg(
@@ -268,13 +312,17 @@ def test_run_arbitrary_borg_without_borg_specific_flags_does_not_raise():
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg',),
         ('borg',),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
     )
 
 
     module.run_arbitrary_borg(
     module.run_arbitrary_borg(
@@ -290,13 +338,17 @@ def test_run_arbitrary_borg_passes_key_sub_command_to_borg_before_injected_flags
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'key', 'export', '--info', '::'),
         ('borg', 'key', 'export', '--info', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
     )
     insert_logging_mock(logging.INFO)
     insert_logging_mock(logging.INFO)
 
 
@@ -313,13 +365,17 @@ def test_run_arbitrary_borg_passes_debug_sub_command_to_borg_before_injected_fla
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'debug', 'dump-manifest', '--info', '::', 'path'),
         ('borg', 'debug', 'dump-manifest', '--info', '::', 'path'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
     )
     insert_logging_mock(logging.INFO)
     insert_logging_mock(logging.INFO)
 
 
@@ -329,3 +385,29 @@ def test_run_arbitrary_borg_passes_debug_sub_command_to_borg_before_injected_fla
         local_borg_version='1.2.3',
         local_borg_version='1.2.3',
         options=['debug', 'dump-manifest', '::', 'path'],
         options=['debug', 'dump-manifest', '::', 'path'],
     )
     )
+
+
+def test_run_arbitrary_borg_calls_borg_with_working_directory():
+    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.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        '/working/dir',
+    )
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'break-lock', '::'),
+        output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
+        shell=True,
+        extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory='/working/dir',
+        borg_local_path='borg',
+        borg_exit_codes=None,
+    )
+
+    module.run_arbitrary_borg(
+        repository_path='repo',
+        config={},
+        local_borg_version='1.2.3',
+        options=['break-lock', '::'],
+    )

+ 18 - 2
tests/unit/borg/test_break_lock.py

@@ -7,13 +7,17 @@ from borgmatic.borg import break_lock as module
 from ..test_verbosity import insert_logging_mock
 from ..test_verbosity import insert_logging_mock
 
 
 
 
-def insert_execute_command_mock(command, borg_exit_codes=None):
+def insert_execute_command_mock(command, working_directory=None, borg_exit_codes=None):
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        working_directory,
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         command,
         command,
+        extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=command[0],
         borg_local_path=command[0],
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     ).once()
     ).once()
 
 
 
 
@@ -128,3 +132,15 @@ def test_break_lock_with_log_debug_calls_borg_with_debug_flags():
         local_borg_version='1.2.3',
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
         global_arguments=flexmock(log_json=False),
     )
     )
+
+
+def test_break_lock_calls_borg_with_working_directory():
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    insert_execute_command_mock(('borg', 'break-lock', 'repo'), working_directory='/working/dir')
+
+    module.break_lock(
+        repository_path='repo',
+        config={'working_directory': '/working/dir'},
+        local_borg_version='1.2.3',
+        global_arguments=flexmock(log_json=False),
+    )

+ 31 - 2
tests/unit/borg/test_change_passphrase.py

@@ -9,18 +9,26 @@ from ..test_verbosity import insert_logging_mock
 
 
 
 
 def insert_execute_command_mock(
 def insert_execute_command_mock(
-    command, config=None, output_file=module.borgmatic.execute.DO_NOT_CAPTURE, borg_exit_codes=None
+    command,
+    config=None,
+    output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
+    working_directory=None,
+    borg_exit_codes=None,
 ):
 ):
     borgmatic.logger.add_custom_log_levels()
     borgmatic.logger.add_custom_log_levels()
 
 
     flexmock(module.environment).should_receive('make_environment').with_args(config or {}).once()
     flexmock(module.environment).should_receive('make_environment').with_args(config or {}).once()
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        working_directory,
+    )
     flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
     flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
         command,
         command,
         output_file=output_file,
         output_file=output_file,
         output_log_level=module.logging.ANSWER,
         output_log_level=module.logging.ANSWER,
+        extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=command[0],
         borg_local_path=command[0],
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     ).once()
     ).once()
 
 
 
 
@@ -161,6 +169,9 @@ def test_change_passphrase_with_log_debug_calls_borg_with_debug_flags():
 
 
 def test_change_passphrase_with_dry_run_skips_borg_call():
 def test_change_passphrase_with_dry_run_skips_borg_call():
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive('execute_command').never()
     flexmock(module.borgmatic.execute).should_receive('execute_command').never()
 
 
     module.change_passphrase(
     module.change_passphrase(
@@ -189,3 +200,21 @@ def test_change_passphrase_calls_borg_without_passphrase():
         change_passphrase_arguments=flexmock(),
         change_passphrase_arguments=flexmock(),
         global_arguments=flexmock(dry_run=False, log_json=False),
         global_arguments=flexmock(dry_run=False, log_json=False),
     )
     )
+
+
+def test_change_passphrase_calls_borg_with_working_directory():
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    config = {'working_directory': '/working/dir'}
+    insert_execute_command_mock(
+        ('borg', 'key', 'change-passphrase', 'repo'),
+        config=config,
+        working_directory='/working/dir',
+    )
+
+    module.change_passphrase(
+        repository_path='repo',
+        config=config,
+        local_borg_version='1.2.3',
+        change_passphrase_arguments=flexmock(),
+        global_arguments=flexmock(dry_run=False, log_json=False),
+    )

+ 57 - 1
tests/unit/borg/test_check.py

@@ -8,11 +8,15 @@ from borgmatic.borg import check as module
 from ..test_verbosity import insert_logging_mock
 from ..test_verbosity import insert_logging_mock
 
 
 
 
-def insert_execute_command_mock(command, borg_exit_codes=None):
+def insert_execute_command_mock(command, working_directory=None, borg_exit_codes=None):
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        working_directory,
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         command,
         command,
         extra_environment=None,
         extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=command[0],
         borg_local_path=command[0],
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
     ).once()
     ).once()
@@ -335,10 +339,14 @@ def test_check_archives_with_progress_passes_through_to_borg():
     flexmock(module).should_receive('make_check_name_flags').and_return(())
     flexmock(module).should_receive('make_check_name_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',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'check', '--progress', 'repo'),
         ('borg', 'check', '--progress', 'repo'),
         output_file=module.DO_NOT_CAPTURE,
         output_file=module.DO_NOT_CAPTURE,
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).once()
     ).once()
@@ -366,10 +374,14 @@ def test_check_archives_with_repair_passes_through_to_borg():
     flexmock(module).should_receive('make_check_name_flags').and_return(())
     flexmock(module).should_receive('make_check_name_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',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'check', '--repair', 'repo'),
         ('borg', 'check', '--repair', 'repo'),
         output_file=module.DO_NOT_CAPTURE,
         output_file=module.DO_NOT_CAPTURE,
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).once()
     ).once()
@@ -397,9 +409,13 @@ def test_check_archives_with_max_duration_flag_passes_through_to_borg():
     flexmock(module).should_receive('make_check_name_flags').and_return(())
     flexmock(module).should_receive('make_check_name_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',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'check', '--max-duration', '33', 'repo'),
         ('borg', 'check', '--max-duration', '33', 'repo'),
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).once()
     ).once()
@@ -450,9 +466,13 @@ def test_check_archives_with_max_duration_option_passes_through_to_borg():
     flexmock(module).should_receive('make_check_name_flags').and_return(())
     flexmock(module).should_receive('make_check_name_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',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'check', '--max-duration', '33', 'repo'),
         ('borg', 'check', '--max-duration', '33', 'repo'),
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).once()
     ).once()
@@ -503,9 +523,13 @@ def test_check_archives_with_max_duration_flag_overrides_max_duration_option():
     flexmock(module).should_receive('make_check_name_flags').and_return(())
     flexmock(module).should_receive('make_check_name_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',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'check', '--max-duration', '44', 'repo'),
         ('borg', 'check', '--max-duration', '44', 'repo'),
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).once()
     ).once()
@@ -796,9 +820,13 @@ def test_check_archives_with_match_archives_passes_through_to_borg():
     )
     )
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'check', '--match-archives', 'foo-*', 'repo'),
         ('borg', 'check', '--match-archives', 'foo-*', 'repo'),
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).once()
     ).once()
@@ -819,3 +847,31 @@ def test_check_archives_with_match_archives_passes_through_to_borg():
         checks={'archives'},
         checks={'archives'},
         archive_filter_flags=('--match-archives', 'foo-*'),
         archive_filter_flags=('--match-archives', 'foo-*'),
     )
     )
+
+
+def test_check_archives_calls_borg_with_working_directory():
+    config = {'working_directory': '/working/dir'}
+    flexmock(module).should_receive('make_check_name_flags').and_return(())
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
+    insert_execute_command_mock(('borg', 'check', 'repo'), working_directory='/working/dir')
+
+    module.check_archives(
+        repository_path='repo',
+        config=config,
+        local_borg_version='1.2.3',
+        check_arguments=flexmock(
+            progress=False,
+            repair=None,
+            only_checks=None,
+            force=None,
+            match_archives=None,
+            max_duration=None,
+        ),
+        global_arguments=flexmock(log_json=False),
+        checks={'repository'},
+        archive_filter_flags=(),
+    )

+ 23 - 2
tests/unit/borg/test_compact.py

@@ -7,14 +7,20 @@ from borgmatic.borg import compact as module
 from ..test_verbosity import insert_logging_mock
 from ..test_verbosity import insert_logging_mock
 
 
 
 
-def insert_execute_command_mock(compact_command, output_log_level, borg_exit_codes=None):
+def insert_execute_command_mock(
+    compact_command, output_log_level, working_directory=None, borg_exit_codes=None
+):
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        working_directory
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         compact_command,
         compact_command,
         output_log_level=output_log_level,
         output_log_level=output_log_level,
+        extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=compact_command[0],
         borg_local_path=compact_command[0],
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     ).once()
     ).once()
 
 
 
 
@@ -212,3 +218,18 @@ def test_compact_segments_with_extra_borg_options_calls_borg_with_extra_options(
         local_borg_version='1.2.3',
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
         global_arguments=flexmock(log_json=False),
     )
     )
+
+
+def test_compact_segments_calls_borg_with_working_directory():
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    insert_execute_command_mock(
+        COMPACT_COMMAND + ('repo',), logging.INFO, working_directory='/working/dir'
+    )
+
+    module.compact_segments(
+        dry_run=False,
+        repository_path='repo',
+        config={'working_directory': '/working/dir'},
+        local_borg_version='1.2.3',
+        global_arguments=flexmock(log_json=False),
+    )

+ 214 - 23
tests/unit/borg/test_create.py

@@ -13,7 +13,7 @@ def test_expand_directory_with_basic_path_passes_it_through():
     flexmock(module.os.path).should_receive('expanduser').and_return('foo')
     flexmock(module.os.path).should_receive('expanduser').and_return('foo')
     flexmock(module.glob).should_receive('glob').and_return([])
     flexmock(module.glob).should_receive('glob').and_return([])
 
 
-    paths = module.expand_directory('foo')
+    paths = module.expand_directory('foo', None)
 
 
     assert paths == ['foo']
     assert paths == ['foo']
 
 
@@ -22,14 +22,36 @@ def test_expand_directory_with_glob_expands():
     flexmock(module.os.path).should_receive('expanduser').and_return('foo*')
     flexmock(module.os.path).should_receive('expanduser').and_return('foo*')
     flexmock(module.glob).should_receive('glob').and_return(['foo', 'food'])
     flexmock(module.glob).should_receive('glob').and_return(['foo', 'food'])
 
 
-    paths = module.expand_directory('foo*')
+    paths = module.expand_directory('foo*', None)
 
 
     assert paths == ['foo', 'food']
     assert paths == ['foo', 'food']
 
 
 
 
+def test_expand_directory_with_working_directory_passes_it_through():
+    flexmock(module.os.path).should_receive('expanduser').and_return('foo')
+    flexmock(module.glob).should_receive('glob').with_args('/working/dir/foo').and_return([]).once()
+
+    paths = module.expand_directory('foo', working_directory='/working/dir')
+
+    assert paths == ['/working/dir/foo']
+
+
+def test_expand_directory_with_glob_passes_through_working_directory():
+    flexmock(module.os.path).should_receive('expanduser').and_return('foo*')
+    flexmock(module.glob).should_receive('glob').with_args('/working/dir/foo*').and_return(
+        ['/working/dir/foo', '/working/dir/food']
+    ).once()
+
+    paths = module.expand_directory('foo*', working_directory='/working/dir')
+
+    assert paths == ['/working/dir/foo', '/working/dir/food']
+
+
 def test_expand_directories_flattens_expanded_directories():
 def test_expand_directories_flattens_expanded_directories():
-    flexmock(module).should_receive('expand_directory').with_args('~/foo').and_return(['/root/foo'])
-    flexmock(module).should_receive('expand_directory').with_args('bar*').and_return(
+    flexmock(module).should_receive('expand_directory').with_args('~/foo', None).and_return(
+        ['/root/foo']
+    )
+    flexmock(module).should_receive('expand_directory').with_args('bar*', None).and_return(
         ['bar', 'barf']
         ['bar', 'barf']
     )
     )
 
 
@@ -38,8 +60,18 @@ def test_expand_directories_flattens_expanded_directories():
     assert paths == ('/root/foo', 'bar', 'barf')
     assert paths == ('/root/foo', 'bar', 'barf')
 
 
 
 
+def test_expand_directories_with_working_directory_passes_it_through():
+    flexmock(module).should_receive('expand_directory').with_args('foo', '/working/dir').and_return(
+        ['/working/dir/foo']
+    )
+
+    paths = module.expand_directories(('foo',), working_directory='/working/dir')
+
+    assert paths == ('/working/dir/foo',)
+
+
 def test_expand_directories_considers_none_as_no_directories():
 def test_expand_directories_considers_none_as_no_directories():
-    paths = module.expand_directories(None)
+    paths = module.expand_directories(None, None)
 
 
     assert paths == ()
     assert paths == ()
 
 
@@ -60,6 +92,7 @@ def test_expand_home_directories_considers_none_as_no_directories():
 
 
 
 
 def test_map_directories_to_devices_gives_device_id_per_path():
 def test_map_directories_to_devices_gives_device_id_per_path():
+    flexmock(module.os.path).should_receive('exists').and_return(True)
     flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55))
     flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55))
     flexmock(module.os).should_receive('stat').with_args('/bar').and_return(flexmock(st_dev=66))
     flexmock(module.os).should_receive('stat').with_args('/bar').and_return(flexmock(st_dev=66))
 
 
@@ -72,8 +105,9 @@ def test_map_directories_to_devices_gives_device_id_per_path():
 
 
 
 
 def test_map_directories_to_devices_with_missing_path_does_not_error():
 def test_map_directories_to_devices_with_missing_path_does_not_error():
+    flexmock(module.os.path).should_receive('exists').and_return(True).and_return(False)
     flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55))
     flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55))
-    flexmock(module.os).should_receive('stat').with_args('/bar').and_raise(FileNotFoundError)
+    flexmock(module.os).should_receive('stat').with_args('/bar').never()
 
 
     device_map = module.map_directories_to_devices(('/foo', '/bar'))
     device_map = module.map_directories_to_devices(('/foo', '/bar'))
 
 
@@ -83,6 +117,23 @@ def test_map_directories_to_devices_with_missing_path_does_not_error():
     }
     }
 
 
 
 
+def test_map_directories_to_devices_uses_working_directory_to_construct_path():
+    flexmock(module.os.path).should_receive('exists').and_return(True)
+    flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55))
+    flexmock(module.os).should_receive('stat').with_args('/working/dir/bar').and_return(
+        flexmock(st_dev=66)
+    )
+
+    device_map = module.map_directories_to_devices(
+        ('/foo', 'bar'), working_directory='/working/dir'
+    )
+
+    assert device_map == {
+        '/foo': 55,
+        'bar': 66,
+    }
+
+
 @pytest.mark.parametrize(
 @pytest.mark.parametrize(
     'directories,additional_directories,expected_directories',
     'directories,additional_directories,expected_directories',
     (
     (
@@ -475,6 +526,9 @@ REPO_ARCHIVE_WITH_PATHS = (f'repo::{DEFAULT_ARCHIVE_NAME}', 'foo', 'bar')
 
 
 
 
 def test_make_base_create_produces_borg_command():
 def test_make_base_create_produces_borg_command():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').and_return(())
     flexmock(module).should_receive('expand_directories').and_return(())
@@ -513,6 +567,9 @@ def test_make_base_create_produces_borg_command():
 
 
 
 
 def test_make_base_create_command_includes_patterns_file_in_borg_command():
 def test_make_base_create_command_includes_patterns_file_in_borg_command():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').and_return(())
     flexmock(module).should_receive('expand_directories').and_return(())
@@ -556,15 +613,19 @@ def test_make_base_create_command_includes_patterns_file_in_borg_command():
 
 
 
 
 def test_make_base_create_command_includes_sources_and_config_paths_in_borg_command():
 def test_make_base_create_command_includes_sources_and_config_paths_in_borg_command():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('deduplicate_directories').and_return(
     flexmock(module).should_receive('deduplicate_directories').and_return(
         ('foo', 'bar', '/tmp/test.yaml')
         ('foo', 'bar', '/tmp/test.yaml')
     )
     )
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
-    flexmock(module).should_receive('expand_directories').with_args([]).and_return(())
+    flexmock(module).should_receive('expand_directories').with_args([], None).and_return(())
     flexmock(module).should_receive('expand_directories').with_args(
     flexmock(module).should_receive('expand_directories').with_args(
-        ('foo', 'bar', '/tmp/test.yaml')
+        ('foo', 'bar', '/tmp/test.yaml'),
+        None,
     ).and_return(('foo', 'bar', '/tmp/test.yaml'))
     ).and_return(('foo', 'bar', '/tmp/test.yaml'))
-    flexmock(module).should_receive('expand_directories').with_args([]).and_return(())
+    flexmock(module).should_receive('expand_directories').with_args([], None).and_return(())
     flexmock(module).should_receive('pattern_root_directories').and_return([])
     flexmock(module).should_receive('pattern_root_directories').and_return([])
     flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
     flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('expand_home_directories').and_return(())
@@ -600,13 +661,16 @@ def test_make_base_create_command_includes_sources_and_config_paths_in_borg_comm
 
 
 
 
 def test_make_base_create_command_with_store_config_false_omits_config_files():
 def test_make_base_create_command_with_store_config_false_omits_config_files():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
-    flexmock(module).should_receive('expand_directories').with_args([]).and_return(())
-    flexmock(module).should_receive('expand_directories').with_args(('foo', 'bar')).and_return(
-        ('foo', 'bar')
-    )
-    flexmock(module).should_receive('expand_directories').with_args([]).and_return(())
+    flexmock(module).should_receive('expand_directories').with_args([], None).and_return(())
+    flexmock(module).should_receive('expand_directories').with_args(
+        ('foo', 'bar'), None
+    ).and_return(('foo', 'bar'))
+    flexmock(module).should_receive('expand_directories').with_args([], None).and_return(())
     flexmock(module).should_receive('pattern_root_directories').and_return([])
     flexmock(module).should_receive('pattern_root_directories').and_return([])
     flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
     flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
     flexmock(module).should_receive('expand_home_directories').and_return(())
     flexmock(module).should_receive('expand_home_directories').and_return(())
@@ -643,6 +707,9 @@ def test_make_base_create_command_with_store_config_false_omits_config_files():
 
 
 
 
 def test_make_base_create_command_includes_exclude_patterns_in_borg_command():
 def test_make_base_create_command_includes_exclude_patterns_in_borg_command():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').and_return(())
     flexmock(module).should_receive('expand_directories').and_return(())
@@ -717,6 +784,9 @@ def test_make_base_create_command_includes_exclude_patterns_in_borg_command():
 def test_make_base_create_command_includes_configuration_option_as_command_flag(
 def test_make_base_create_command_includes_configuration_option_as_command_flag(
     option_name, option_value, feature_available, option_flags
     option_name, option_value, feature_available, option_flags
 ):
 ):
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').and_return(())
     flexmock(module).should_receive('expand_directories').and_return(())
@@ -756,6 +826,9 @@ def test_make_base_create_command_includes_configuration_option_as_command_flag(
 
 
 
 
 def test_make_base_create_command_includes_dry_run_in_borg_command():
 def test_make_base_create_command_includes_dry_run_in_borg_command():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').and_return(())
     flexmock(module).should_receive('expand_directories').and_return(())
@@ -795,6 +868,9 @@ def test_make_base_create_command_includes_dry_run_in_borg_command():
 
 
 
 
 def test_make_base_create_command_includes_local_path_in_borg_command():
 def test_make_base_create_command_includes_local_path_in_borg_command():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').and_return(())
     flexmock(module).should_receive('expand_directories').and_return(())
@@ -834,6 +910,9 @@ def test_make_base_create_command_includes_local_path_in_borg_command():
 
 
 
 
 def test_make_base_create_command_includes_remote_path_in_borg_command():
 def test_make_base_create_command_includes_remote_path_in_borg_command():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').and_return(())
     flexmock(module).should_receive('expand_directories').and_return(())
@@ -873,6 +952,9 @@ def test_make_base_create_command_includes_remote_path_in_borg_command():
 
 
 
 
 def test_make_base_create_command_includes_log_json_in_borg_command():
 def test_make_base_create_command_includes_log_json_in_borg_command():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').and_return(())
     flexmock(module).should_receive('expand_directories').and_return(())
@@ -911,6 +993,9 @@ def test_make_base_create_command_includes_log_json_in_borg_command():
 
 
 
 
 def test_make_base_create_command_includes_list_flags_in_borg_command():
 def test_make_base_create_command_includes_list_flags_in_borg_command():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').and_return(())
     flexmock(module).should_receive('expand_directories').and_return(())
@@ -950,6 +1035,9 @@ def test_make_base_create_command_includes_list_flags_in_borg_command():
 
 
 
 
 def test_make_base_create_command_with_stream_processes_ignores_read_special_false_and_excludes_special_files():
 def test_make_base_create_command_with_stream_processes_ignores_read_special_false_and_excludes_special_files():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').and_return(())
     flexmock(module).should_receive('expand_directories').and_return(())
@@ -996,6 +1084,9 @@ def test_make_base_create_command_with_stream_processes_ignores_read_special_fal
 
 
 
 
 def test_make_base_create_command_with_stream_processes_and_read_special_true_skip_special_files_excludes():
 def test_make_base_create_command_with_stream_processes_and_read_special_true_skip_special_files_excludes():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').and_return(())
     flexmock(module).should_receive('expand_directories').and_return(())
@@ -1038,6 +1129,9 @@ def test_make_base_create_command_with_stream_processes_and_read_special_true_sk
 
 
 
 
 def test_make_base_create_command_with_non_matching_source_directories_glob_passes_through():
 def test_make_base_create_command_with_non_matching_source_directories_glob_passes_through():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo*',))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo*',))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1077,6 +1171,9 @@ def test_make_base_create_command_with_non_matching_source_directories_glob_pass
 
 
 
 
 def test_make_base_create_command_expands_glob_in_source_directories():
 def test_make_base_create_command_expands_glob_in_source_directories():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'food'))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'food'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1116,6 +1213,9 @@ def test_make_base_create_command_expands_glob_in_source_directories():
 
 
 
 
 def test_make_base_create_command_includes_archive_name_format_in_borg_command():
 def test_make_base_create_command_includes_archive_name_format_in_borg_command():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -1236,6 +1336,9 @@ def test_base_create_command_includes_repository_and_archive_name_format_with_pl
 
 
 
 
 def test_make_base_create_command_includes_extra_borg_options_in_borg_command():
 def test_make_base_create_command_includes_extra_borg_options_in_borg_command():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('expand_directories').and_return(())
     flexmock(module).should_receive('expand_directories').and_return(())
@@ -1275,6 +1378,9 @@ def test_make_base_create_command_includes_extra_borg_options_in_borg_command():
 
 
 
 
 def test_make_base_create_command_with_non_existent_directory_and_source_directories_must_exist_raises():
 def test_make_base_create_command_with_non_existent_directory_and_source_directories_must_exist_raises():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('check_all_source_directories_exist').and_raise(ValueError)
     flexmock(module).should_receive('check_all_source_directories_exist').and_raise(ValueError)
 
 
     with pytest.raises(ValueError):
     with pytest.raises(ValueError):
@@ -1302,6 +1408,9 @@ def test_create_archive_calls_borg_with_parameters():
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS,
         ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_log_level=logging.INFO,
@@ -1336,6 +1445,9 @@ def test_create_archive_calls_borg_with_environment():
     )
     )
     environment = {'BORG_THINGY': 'YUP'}
     environment = {'BORG_THINGY': 'YUP'}
     flexmock(module.environment).should_receive('make_environment').and_return(environment)
     flexmock(module.environment).should_receive('make_environment').and_return(environment)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS,
         ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_log_level=logging.INFO,
@@ -1369,6 +1481,9 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter():
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--info') + REPO_ARCHIVE_WITH_PATHS,
         ('borg', 'create', '--info') + REPO_ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_log_level=logging.INFO,
@@ -1403,6 +1518,9 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
         ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
         working_directory=None,
         working_directory=None,
@@ -1436,6 +1554,9 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--debug', '--show-rc') + REPO_ARCHIVE_WITH_PATHS,
         ('borg', 'create', '--debug', '--show-rc') + REPO_ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_log_level=logging.INFO,
@@ -1470,6 +1591,9 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
         ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
         working_directory=None,
         working_directory=None,
@@ -1505,6 +1629,9 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats():
         (('borg', 'create', '--dry-run'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
         (('borg', 'create', '--dry-run'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--dry-run', '--info') + REPO_ARCHIVE_WITH_PATHS,
         ('borg', 'create', '--dry-run', '--info') + REPO_ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_log_level=logging.INFO,
@@ -1540,6 +1667,9 @@ def test_create_archive_with_working_directory_calls_borg_with_working_directory
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        '/working/dir'
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS,
         ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_log_level=logging.INFO,
@@ -1575,6 +1705,9 @@ def test_create_archive_with_exit_codes_calls_borg_using_them():
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
     borg_exit_codes = flexmock()
     borg_exit_codes = flexmock()
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS,
         ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_log_level=logging.INFO,
@@ -1609,6 +1742,9 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_answer_ou
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--stats') + REPO_ARCHIVE_WITH_PATHS,
         ('borg', 'create', '--stats') + REPO_ARCHIVE_WITH_PATHS,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
@@ -1648,6 +1784,9 @@ def test_create_archive_with_files_calls_borg_with_answer_output_log_level():
         )
         )
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--list', '--filter', 'FOO') + REPO_ARCHIVE_WITH_PATHS,
         ('borg', 'create', '--list', '--filter', 'FOO') + REPO_ARCHIVE_WITH_PATHS,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
@@ -1682,6 +1821,9 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--info', '--progress') + REPO_ARCHIVE_WITH_PATHS,
         ('borg', 'create', '--info', '--progress') + REPO_ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_log_level=logging.INFO,
@@ -1717,6 +1859,9 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter():
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--progress') + REPO_ARCHIVE_WITH_PATHS,
         ('borg', 'create', '--progress') + REPO_ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
         output_log_level=logging.INFO,
@@ -1763,6 +1908,9 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr
         '--read-special',
         '--read-special',
         '--progress',
         '--progress',
     ) + REPO_ARCHIVE_WITH_PATHS
     ) + REPO_ARCHIVE_WITH_PATHS
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_with_processes').with_args(
     flexmock(module).should_receive('execute_command_with_processes').with_args(
         create_command + ('--dry-run', '--list'),
         create_command + ('--dry-run', '--list'),
         processes=processes,
         processes=processes,
@@ -1809,6 +1957,9 @@ def test_create_archive_with_json_calls_borg_with_json_flag():
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
         ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
         working_directory=None,
         working_directory=None,
@@ -1843,6 +1994,9 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_flag():
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
         ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
         working_directory=None,
         working_directory=None,
@@ -1869,11 +2023,48 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_flag():
     assert json_output == '[]'
     assert json_output == '[]'
 
 
 
 
+def test_create_archive_calls_borg_with_working_directory():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+    flexmock(module).should_receive('expand_directories').and_return(())
+    flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
+    flexmock(module).should_receive('make_base_create_command').and_return(
+        (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
+    )
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        '/working/dir'
+    )
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS,
+        output_log_level=logging.INFO,
+        output_file=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
+        working_directory='/working/dir',
+        extra_environment=None,
+    )
+
+    module.create_archive(
+        dry_run=False,
+        repository_path='repo',
+        config={
+            'source_directories': ['foo', 'bar'],
+            'repositories': ['repo'],
+            'exclude_patterns': None,
+            'working_directory': '/working/dir',
+        },
+        config_paths=['/tmp/test.yaml'],
+        local_borg_version='1.2.3',
+        global_arguments=flexmock(log_json=False),
+    )
+
+
 def test_check_all_source_directories_exist_with_glob_and_tilde_directories():
 def test_check_all_source_directories_exist_with_glob_and_tilde_directories():
-    flexmock(module).should_receive('expand_directory').with_args('foo*').and_return(
+    flexmock(module).should_receive('expand_directory').with_args('foo*', None).and_return(
         ('foo', 'food')
         ('foo', 'food')
     )
     )
-    flexmock(module).should_receive('expand_directory').with_args('~/bar').and_return(
+    flexmock(module).should_receive('expand_directory').with_args('~/bar', None).and_return(
         ('/root/bar',)
         ('/root/bar',)
     )
     )
     flexmock(module.os.path).should_receive('exists').and_return(False)
     flexmock(module.os.path).should_receive('exists').and_return(False)
@@ -1885,7 +2076,7 @@ def test_check_all_source_directories_exist_with_glob_and_tilde_directories():
 
 
 
 
 def test_check_all_source_directories_exist_with_non_existent_directory_raises():
 def test_check_all_source_directories_exist_with_non_existent_directory_raises():
-    flexmock(module).should_receive('expand_directory').with_args('foo').and_return(('foo',))
+    flexmock(module).should_receive('expand_directory').with_args('foo', None).and_return(('foo',))
     flexmock(module.os.path).should_receive('exists').and_return(False)
     flexmock(module.os.path).should_receive('exists').and_return(False)
 
 
     with pytest.raises(ValueError):
     with pytest.raises(ValueError):
@@ -1893,12 +2084,12 @@ def test_check_all_source_directories_exist_with_non_existent_directory_raises()
 
 
 
 
 def test_check_all_source_directories_exist_with_working_directory_applies_to_relative_source_directories():
 def test_check_all_source_directories_exist_with_working_directory_applies_to_relative_source_directories():
-    flexmock(module).should_receive('expand_directory').with_args('/tmp/foo*').and_return(
-        ('/tmp/foo', '/tmp/food')
-    )
-    flexmock(module).should_receive('expand_directory').with_args('/root/bar').and_return(
-        ('/root/bar',)
-    )
+    flexmock(module).should_receive('expand_directory').with_args(
+        'foo*', working_directory='/tmp'
+    ).and_return(('/tmp/foo', '/tmp/food'))
+    flexmock(module).should_receive('expand_directory').with_args(
+        '/root/bar', working_directory='/tmp'
+    ).and_return(('/root/bar',))
     flexmock(module.os.path).should_receive('exists').and_return(False)
     flexmock(module.os.path).should_receive('exists').and_return(False)
     flexmock(module.os.path).should_receive('exists').with_args('/tmp/foo').and_return(True)
     flexmock(module.os.path).should_receive('exists').with_args('/tmp/foo').and_return(True)
     flexmock(module.os.path).should_receive('exists').with_args('/tmp/food').and_return(True)
     flexmock(module.os.path).should_receive('exists').with_args('/tmp/food').and_return(True)

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

@@ -271,6 +271,9 @@ def test_delete_archives_with_archive_calls_borg_delete():
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
         flexmock()
         flexmock()
     )
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive('execute_command').once()
     flexmock(module.borgmatic.execute).should_receive('execute_command').once()
 
 
     module.delete_archives(
     module.delete_archives(
@@ -289,6 +292,9 @@ def test_delete_archives_with_match_archives_calls_borg_delete():
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
         flexmock()
         flexmock()
     )
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive('execute_command').once()
     flexmock(module.borgmatic.execute).should_receive('execute_command').once()
 
 
     module.delete_archives(
     module.delete_archives(
@@ -308,6 +314,9 @@ def test_delete_archives_with_archive_related_argument_calls_borg_delete(argumen
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
         flexmock()
         flexmock()
     )
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive('execute_command').once()
     flexmock(module.borgmatic.execute).should_receive('execute_command').once()
 
 
     module.delete_archives(
     module.delete_archives(
@@ -325,6 +334,9 @@ def test_delete_archives_without_archive_related_argument_calls_borg_repo_delete
     flexmock(module.borgmatic.borg.repo_delete).should_receive('delete_repository').once()
     flexmock(module.borgmatic.borg.repo_delete).should_receive('delete_repository').once()
     flexmock(module).should_receive('make_delete_command').never()
     flexmock(module).should_receive('make_delete_command').never()
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').never()
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').never()
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive('execute_command').never()
     flexmock(module.borgmatic.execute).should_receive('execute_command').never()
 
 
     module.delete_archives(
     module.delete_archives(
@@ -336,3 +348,33 @@ def test_delete_archives_without_archive_related_argument_calls_borg_repo_delete
         ),
         ),
         global_arguments=flexmock(),
         global_arguments=flexmock(),
     )
     )
+
+
+def test_delete_archives_calls_borg_delete_with_working_directory():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.borgmatic.borg.repo_delete).should_receive('delete_repository').never()
+    command = flexmock()
+    flexmock(module).should_receive('make_delete_command').and_return(command)
+    extra_environment = flexmock()
+    flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
+        extra_environment
+    )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        '/working/dir'
+    )
+    flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
+        command,
+        output_log_level=logging.ANSWER,
+        extra_environment=extra_environment,
+        working_directory='/working/dir',
+        borg_local_path='borg',
+        borg_exit_codes=None,
+    ).once()
+
+    module.delete_archives(
+        repository={'path': 'repo'},
+        config={'working_directory': '/working/dir'},
+        local_borg_version=flexmock(),
+        delete_arguments=flexmock(archive='archive'),
+        global_arguments=flexmock(),
+    )

+ 43 - 3
tests/unit/borg/test_export_key.py

@@ -9,17 +9,23 @@ from borgmatic.borg import export_key as module
 from ..test_verbosity import insert_logging_mock
 from ..test_verbosity import insert_logging_mock
 
 
 
 
-def insert_execute_command_mock(command, output_file=module.DO_NOT_CAPTURE, borg_exit_codes=None):
+def insert_execute_command_mock(
+    command, output_file=module.DO_NOT_CAPTURE, working_directory=None, borg_exit_codes=None
+):
     borgmatic.logger.add_custom_log_levels()
     borgmatic.logger.add_custom_log_levels()
 
 
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        working_directory,
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         command,
         command,
         output_file=output_file,
         output_file=output_file,
         output_log_level=module.logging.ANSWER,
         output_log_level=module.logging.ANSWER,
+        extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=command[0],
         borg_local_path=command[0],
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     ).once()
     ).once()
 
 
 
 
@@ -198,7 +204,7 @@ def test_export_key_calls_borg_with_qr_html_flag():
 
 
 def test_export_key_calls_borg_with_path_argument():
 def test_export_key_calls_borg_with_path_argument():
     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').and_return(False)
+    flexmock(module.os.path).should_receive('exists').with_args('dest').and_return(False)
     insert_execute_command_mock(('borg', 'key', 'export', 'repo', 'dest'), output_file=None)
     insert_execute_command_mock(('borg', 'key', 'export', 'repo', 'dest'), output_file=None)
 
 
     module.export_key(
     module.export_key(
@@ -251,3 +257,37 @@ def test_export_key_with_dry_run_skips_borg_call():
         export_arguments=flexmock(paper=False, qr_html=False, path=None),
         export_arguments=flexmock(paper=False, qr_html=False, path=None),
         global_arguments=flexmock(dry_run=True, log_json=False),
         global_arguments=flexmock(dry_run=True, log_json=False),
     )
     )
+
+
+def test_export_key_calls_borg_with_working_directory():
+    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', 'repo'), working_directory='/working/dir')
+
+    module.export_key(
+        repository_path='repo',
+        config={'working_directory': '/working/dir'},
+        local_borg_version='1.2.3',
+        export_arguments=flexmock(paper=False, qr_html=False, path=None),
+        global_arguments=flexmock(dry_run=False, log_json=False),
+    )
+
+
+def test_export_key_calls_borg_with_path_argument_and_working_directory():
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    flexmock(module.os.path).should_receive('exists').with_args('/working/dir/dest').and_return(
+        False
+    ).once()
+    insert_execute_command_mock(
+        ('borg', 'key', 'export', 'repo', 'dest'),
+        output_file=None,
+        working_directory='/working/dir',
+    )
+
+    module.export_key(
+        repository_path='repo',
+        config={'working_directory': '/working/dir'},
+        local_borg_version='1.2.3',
+        export_arguments=flexmock(paper=False, qr_html=False, path='dest'),
+        global_arguments=flexmock(dry_run=False, log_json=False),
+    )

+ 29 - 1
tests/unit/borg/test_export_tar.py

@@ -10,18 +10,23 @@ from ..test_verbosity import insert_logging_mock
 def insert_execute_command_mock(
 def insert_execute_command_mock(
     command,
     command,
     output_log_level=logging.INFO,
     output_log_level=logging.INFO,
+    working_directory=None,
     borg_local_path='borg',
     borg_local_path='borg',
     borg_exit_codes=None,
     borg_exit_codes=None,
     capture=True,
     capture=True,
 ):
 ):
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        working_directory,
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         command,
         command,
         output_file=None if capture else module.DO_NOT_CAPTURE,
         output_file=None if capture else module.DO_NOT_CAPTURE,
         output_log_level=output_log_level,
         output_log_level=output_log_level,
+        extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=borg_local_path,
         borg_local_path=borg_local_path,
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     ).once()
     ).once()
 
 
 
 
@@ -353,3 +358,26 @@ def test_export_tar_archive_calls_borg_with_stdout_destination_path():
         local_borg_version='1.2.3',
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
         global_arguments=flexmock(log_json=False),
     )
     )
+
+
+def test_export_tar_archive_calls_borg_with_working_directory():
+    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', 'repo::archive', 'test.tar'),
+        working_directory='/working/dir',
+    )
+
+    module.export_tar_archive(
+        dry_run=False,
+        repository_path='repo',
+        archive='archive',
+        paths=[],
+        destination_path='test.tar',
+        config={'working_directory': '/working/dir'},
+        local_borg_version='1.2.3',
+        global_arguments=flexmock(log_json=False),
+    )

+ 119 - 6
tests/unit/borg/test_extract.py

@@ -8,12 +8,12 @@ from borgmatic.borg import extract as module
 from ..test_verbosity import insert_logging_mock
 from ..test_verbosity import insert_logging_mock
 
 
 
 
-def insert_execute_command_mock(command, working_directory=None, borg_exit_codes=None):
+def insert_execute_command_mock(command, destination_path=None, borg_exit_codes=None):
     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(
         command,
         command,
-        working_directory=working_directory,
         extra_environment=None,
         extra_environment=None,
+        working_directory=destination_path,
         borg_local_path=command[0],
         borg_local_path=command[0],
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
     ).once()
     ).once()
@@ -177,6 +177,9 @@ 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'))
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
         ('repo::archive',)
     )
     )
@@ -199,6 +202,9 @@ def test_extract_archive_calls_borg_with_local_path():
     flexmock(module.os.path).should_receive('abspath').and_return('repo')
     flexmock(module.os.path).should_receive('abspath').and_return('repo')
     insert_execute_command_mock(('borg1', 'extract', 'repo::archive'))
     insert_execute_command_mock(('borg1', 'extract', 'repo::archive'))
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
         ('repo::archive',)
     )
     )
@@ -225,6 +231,9 @@ def test_extract_archive_calls_borg_with_exit_codes():
         ('borg', 'extract', 'repo::archive'), borg_exit_codes=borg_exit_codes
         ('borg', 'extract', 'repo::archive'), borg_exit_codes=borg_exit_codes
     )
     )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
         ('repo::archive',)
     )
     )
@@ -247,6 +256,9 @@ def test_extract_archive_calls_borg_with_remote_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', '--remote-path', 'borg1', 'repo::archive'))
     insert_execute_command_mock(('borg', 'extract', '--remote-path', 'borg1', 'repo::archive'))
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
         ('repo::archive',)
     )
     )
@@ -277,6 +289,9 @@ def test_extract_archive_calls_borg_with_numeric_ids_parameter(feature_available
     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', option_flag, 'repo::archive'))
     insert_execute_command_mock(('borg', 'extract', option_flag, 'repo::archive'))
     flexmock(module.feature).should_receive('available').and_return(feature_available)
     flexmock(module.feature).should_receive('available').and_return(feature_available)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
         ('repo::archive',)
     )
     )
@@ -299,6 +314,9 @@ def test_extract_archive_calls_borg_with_umask_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', '--umask', '0770', 'repo::archive'))
     insert_execute_command_mock(('borg', 'extract', '--umask', '0770', 'repo::archive'))
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
         ('repo::archive',)
     )
     )
@@ -321,6 +339,9 @@ def test_extract_archive_calls_borg_with_log_json_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', '--log-json', 'repo::archive'))
     insert_execute_command_mock(('borg', 'extract', '--log-json', 'repo::archive'))
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
         ('repo::archive',)
     )
     )
@@ -340,6 +361,9 @@ def test_extract_archive_calls_borg_with_lock_wait_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', '--lock-wait', '5', 'repo::archive'))
     insert_execute_command_mock(('borg', 'extract', '--lock-wait', '5', 'repo::archive'))
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
         ('repo::archive',)
     )
     )
@@ -363,6 +387,9 @@ def test_extract_archive_with_log_info_calls_borg_with_info_parameter():
     insert_execute_command_mock(('borg', 'extract', '--info', 'repo::archive'))
     insert_execute_command_mock(('borg', 'extract', '--info', 'repo::archive'))
     insert_logging_mock(logging.INFO)
     insert_logging_mock(logging.INFO)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
         ('repo::archive',)
     )
     )
@@ -388,6 +415,9 @@ def test_extract_archive_with_log_debug_calls_borg_with_debug_flags():
     )
     )
     insert_logging_mock(logging.DEBUG)
     insert_logging_mock(logging.DEBUG)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
         ('repo::archive',)
     )
     )
@@ -410,6 +440,9 @@ def test_extract_archive_calls_borg_with_dry_run_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', '--dry-run', 'repo::archive'))
     insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive'))
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
         ('repo::archive',)
     )
     )
@@ -430,8 +463,11 @@ def test_extract_archive_calls_borg_with_dry_run_parameter():
 
 
 def test_extract_archive_calls_borg_with_destination_path():
 def test_extract_archive_calls_borg_with_destination_path():
     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'), working_directory='/dest')
+    insert_execute_command_mock(('borg', 'extract', 'repo::archive'), destination_path='/dest')
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
         ('repo::archive',)
     )
     )
@@ -455,6 +491,9 @@ def test_extract_archive_calls_borg_with_strip_components():
     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', '--strip-components', '5', 'repo::archive'))
     insert_execute_command_mock(('borg', 'extract', '--strip-components', '5', 'repo::archive'))
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
         ('repo::archive',)
     )
     )
@@ -488,6 +527,9 @@ def test_extract_archive_calls_borg_with_strip_components_calculated_from_all():
         )
         )
     )
     )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
         ('repo::archive',)
     )
     )
@@ -521,6 +563,9 @@ def test_extract_archive_calls_borg_with_strip_components_calculated_from_all_wi
         )
         )
     )
     )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
         ('repo::archive',)
     )
     )
@@ -543,6 +588,9 @@ def test_extract_archive_calls_borg_with_strip_components_calculated_from_all_wi
 def test_extract_archive_with_strip_components_all_and_no_paths_raises():
 def test_extract_archive_with_strip_components_all_and_no_paths_raises():
     flexmock(module.os.path).should_receive('abspath').and_return('repo')
     flexmock(module.os.path).should_receive('abspath').and_return('repo')
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
         ('repo::archive',)
     )
     )
@@ -567,15 +615,21 @@ def test_extract_archive_with_strip_components_all_and_no_paths_raises():
 def test_extract_archive_calls_borg_with_progress_parameter():
 def test_extract_archive_calls_borg_with_progress_parameter():
     flexmock(module.os.path).should_receive('abspath').and_return('repo')
     flexmock(module.os.path).should_receive('abspath').and_return('repo')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'extract', '--progress', 'repo::archive'),
         ('borg', 'extract', '--progress', 'repo::archive'),
         output_file=module.DO_NOT_CAPTURE,
         output_file=module.DO_NOT_CAPTURE,
-        working_directory=None,
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).once()
     ).once()
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
         ('repo::archive',)
     )
     )
@@ -616,16 +670,22 @@ def test_extract_archive_calls_borg_with_stdout_parameter_and_returns_process():
     flexmock(module.os.path).should_receive('abspath').and_return('repo')
     flexmock(module.os.path).should_receive('abspath').and_return('repo')
     process = flexmock()
     process = flexmock()
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'extract', '--stdout', 'repo::archive'),
         ('borg', 'extract', '--stdout', 'repo::archive'),
         output_file=module.subprocess.PIPE,
         output_file=module.subprocess.PIPE,
-        working_directory=None,
         run_to_completion=False,
         run_to_completion=False,
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).and_return(process).once()
     ).and_return(process).once()
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('repo::archive',)
         ('repo::archive',)
     )
     )
@@ -651,14 +711,20 @@ def test_extract_archive_calls_borg_with_stdout_parameter_and_returns_process():
 def test_extract_archive_skips_abspath_for_remote_repository():
 def test_extract_archive_skips_abspath_for_remote_repository():
     flexmock(module.os.path).should_receive('abspath').never()
     flexmock(module.os.path).should_receive('abspath').never()
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'extract', 'server:repo::archive'),
         ('borg', 'extract', 'server:repo::archive'),
-        working_directory=None,
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).once()
     ).once()
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('server:repo::archive',)
         ('server:repo::archive',)
     )
     )
@@ -675,3 +741,50 @@ def test_extract_archive_skips_abspath_for_remote_repository():
         local_borg_version='1.2.3',
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
         global_arguments=flexmock(log_json=False),
     )
     )
+
+
+def test_extract_archive_uses_configured_working_directory_in_repo_path_and_destination_path():
+    flexmock(module.os.path).should_receive('abspath').and_return('repo')
+    insert_execute_command_mock(
+        ('borg', 'extract', '/working/dir/repo::archive'), destination_path='/working/dir/dest'
+    )
+    flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
+        ('/working/dir/repo::archive',)
+    )
+    flexmock(module.borgmatic.config.validate).should_receive(
+        'normalize_repository_path'
+    ).with_args('/working/dir/repo').and_return('/working/dir/repo').once()
+
+    module.extract_archive(
+        dry_run=False,
+        repository='repo',
+        archive='archive',
+        paths=None,
+        config={'working_directory': '/working/dir'},
+        local_borg_version='1.2.3',
+        global_arguments=flexmock(log_json=False),
+        destination_path='dest',
+    )
+
+
+def test_extract_archive_uses_configured_working_directory_in_repo_path_when_destination_path_is_not_set():
+    flexmock(module.os.path).should_receive('abspath').and_return('repo')
+    insert_execute_command_mock(('borg', 'extract', '/working/dir/repo::archive'))
+    flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
+        ('/working/dir/repo::archive',)
+    )
+    flexmock(module.borgmatic.config.validate).should_receive(
+        'normalize_repository_path'
+    ).with_args('/working/dir/repo').and_return('/working/dir/repo').once()
+
+    module.extract_archive(
+        dry_run=False,
+        repository='repo',
+        archive='archive',
+        paths=None,
+        config={'working_directory': '/working/dir'},
+        local_borg_version='1.2.3',
+        global_arguments=flexmock(log_json=False),
+    )

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

@@ -446,6 +446,9 @@ def test_display_archives_info_calls_two_commands():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('make_info_command')
     flexmock(module).should_receive('make_info_command')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').once()
     flexmock(module).should_receive('execute_command_and_capture_output').once()
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
     flexmock(module).should_receive('execute_command').once()
     flexmock(module).should_receive('execute_command').once()
@@ -463,6 +466,9 @@ def test_display_archives_info_with_json_calls_json_command_only():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('make_info_command')
     flexmock(module).should_receive('make_info_command')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     json_output = flexmock()
     json_output = flexmock()
     flexmock(module).should_receive('execute_command_and_capture_output').and_return(json_output)
     flexmock(module).should_receive('execute_command_and_capture_output').and_return(json_output)
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never()
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never()
@@ -478,3 +484,36 @@ def test_display_archives_info_with_json_calls_json_command_only():
         )
         )
         == json_output
         == json_output
     )
     )
+
+
+def test_display_archives_info_calls_borg_with_working_directory():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module).should_receive('make_info_command')
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        '/working/dir',
+    )
+    flexmock(module).should_receive('execute_command_and_capture_output').with_args(
+        full_command=object,
+        extra_environment=object,
+        working_directory='/working/dir',
+        borg_local_path=object,
+        borg_exit_codes=object,
+    ).once()
+    flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
+    flexmock(module).should_receive('execute_command').with_args(
+        full_command=object,
+        output_log_level=object,
+        extra_environment=object,
+        working_directory='/working/dir',
+        borg_local_path=object,
+        borg_exit_codes=object,
+    ).once()
+
+    module.display_archives_info(
+        repository_path='repo',
+        config={'working_directory': '/working/dir'},
+        local_borg_version='2.3.4',
+        global_arguments=flexmock(log_json=False),
+        info_arguments=flexmock(archive=None, json=False, prefix=None, match_archives=None),
+    )

+ 94 - 9
tests/unit/borg/test_list.py

@@ -287,6 +287,9 @@ def test_make_find_paths_adds_globs_to_path_fragments():
 
 
 def test_capture_archive_listing_does_not_raise():
 def test_capture_archive_listing_does_not_raise():
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').and_return('')
     flexmock(module).should_receive('execute_command_and_capture_output').and_return('')
     flexmock(module).should_receive('make_list_command')
     flexmock(module).should_receive('make_list_command')
 
 
@@ -328,12 +331,16 @@ def test_list_archive_calls_borg_with_flags():
     ).and_return(('borg', 'list', 'repo::archive'))
     ).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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', 'repo::archive'),
         ('borg', 'list', 'repo::archive'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     ).once()
     ).once()
 
 
     module.list_archive(
     module.list_archive(
@@ -392,12 +399,16 @@ def test_list_archive_calls_borg_with_local_path():
     ).and_return(('borg2', 'list', 'repo::archive'))
     ).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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg2', 'list', 'repo::archive'),
         ('borg2', 'list', 'repo::archive'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg2',
         borg_local_path='borg2',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     ).once()
     ).once()
 
 
     module.list_archive(
     module.list_archive(
@@ -440,12 +451,16 @@ def test_list_archive_calls_borg_using_exit_codes():
     ).and_return(('borg', 'list', 'repo::archive'))
     ).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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', 'repo::archive'),
         ('borg', 'list', 'repo::archive'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     ).once()
     ).once()
 
 
     module.list_archive(
     module.list_archive(
@@ -477,9 +492,13 @@ def test_list_archive_calls_borg_multiple_times_with_find_paths():
     flexmock(module.repo_list).should_receive('make_repo_list_command').and_return(
     flexmock(module.repo_list).should_receive('make_repo_list_command').and_return(
         ('borg', 'list', 'repo')
         ('borg', 'list', 'repo')
     )
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'list', 'repo'),
         ('borg', 'list', 'repo'),
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).and_return('archive1\narchive2').once()
     ).and_return('archive1\narchive2').once()
@@ -491,16 +510,18 @@ def test_list_archive_calls_borg_multiple_times_with_find_paths():
     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,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     ).once()
     ).once()
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', 'repo::archive2') + glob_paths,
         ('borg', 'list', 'repo::archive2') + glob_paths,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     ).once()
     ).once()
 
 
     module.list_archive(
     module.list_archive(
@@ -541,12 +562,16 @@ def test_list_archive_calls_borg_with_archive():
     ).and_return(('borg', 'list', 'repo::archive'))
     ).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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', 'repo::archive'),
         ('borg', 'list', 'repo::archive'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     ).once()
     ).once()
 
 
     module.list_archive(
     module.list_archive(
@@ -662,12 +687,16 @@ def test_list_archive_with_archive_ignores_archive_filter_flag(
     ).and_return(('borg', 'list', 'repo::archive'))
     ).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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', 'repo::archive'),
         ('borg', 'list', 'repo::archive'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     ).once()
     ).once()
 
 
     module.list_archive(
     module.list_archive(
@@ -721,9 +750,13 @@ def test_list_archive_with_find_paths_allows_archive_filter_flag_but_only_passes
         remote_path=None,
         remote_path=None,
     ).and_return(('borg', 'repo-list', '--repo', 'repo'))
     ).and_return(('borg', 'repo-list', '--repo', 'repo'))
 
 
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-list', '--repo', 'repo'),
         ('borg', 'repo-list', '--repo', 'repo'),
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).and_return('archive1\narchive2').once()
     ).and_return('archive1\narchive2').once()
@@ -771,16 +804,18 @@ def test_list_archive_with_find_paths_allows_archive_filter_flag_but_only_passes
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', '--repo', 'repo', 'archive1') + glob_paths,
         ('borg', 'list', '--repo', 'repo', 'archive1') + glob_paths,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     ).once()
     ).once()
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', '--repo', 'repo', 'archive2') + glob_paths,
         ('borg', 'list', '--repo', 'repo', 'archive2') + glob_paths,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     ).once()
     ).once()
 
 
     module.list_archive(
     module.list_archive(
@@ -799,3 +834,53 @@ def test_list_archive_with_find_paths_allows_archive_filter_flag_but_only_passes
         ),
         ),
         global_arguments=global_arguments,
         global_arguments=global_arguments,
     )
     )
+
+
+def test_list_archive_calls_borg_with_working_directory():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+    flexmock(module.logger).answer = lambda message: None
+    list_arguments = argparse.Namespace(
+        archive='archive',
+        paths=None,
+        json=False,
+        find_paths=None,
+        prefix=None,
+        match_archives=None,
+        sort_by=None,
+        first=None,
+        last=None,
+    )
+    global_arguments = flexmock(log_json=False)
+
+    flexmock(module.feature).should_receive('available').and_return(False)
+    flexmock(module).should_receive('make_list_command').with_args(
+        repository_path='repo',
+        config={'working_directory': '/working/dir'},
+        local_borg_version='1.2.3',
+        list_arguments=list_arguments,
+        global_arguments=global_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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        '/working/dir',
+    )
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'list', 'repo::archive'),
+        output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory='/working/dir',
+        borg_local_path='borg',
+        borg_exit_codes=None,
+    ).once()
+
+    module.list_archive(
+        repository_path='repo',
+        config={'working_directory': '/working/dir'},
+        local_borg_version='1.2.3',
+        list_arguments=list_arguments,
+        global_arguments=global_arguments,
+    )

+ 32 - 4
tests/unit/borg/test_mount.py

@@ -7,13 +7,17 @@ from borgmatic.borg import mount as module
 from ..test_verbosity import insert_logging_mock
 from ..test_verbosity import insert_logging_mock
 
 
 
 
-def insert_execute_command_mock(command, borg_exit_codes=None):
+def insert_execute_command_mock(command, working_directory=None, borg_exit_codes=None):
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        working_directory,
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         command,
         command,
+        extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=command[0],
         borg_local_path=command[0],
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     ).once()
     ).once()
 
 
 
 
@@ -254,12 +258,16 @@ def test_mount_archive_calls_borg_with_foreground_parameter():
         ('repo::archive',)
         ('repo::archive',)
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'mount', '--foreground', 'repo::archive', '/mnt'),
         ('borg', 'mount', '--foreground', 'repo::archive', '/mnt'),
         output_file=module.DO_NOT_CAPTURE,
         output_file=module.DO_NOT_CAPTURE,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     ).once()
     ).once()
 
 
     mount_arguments = flexmock(mount_point='/mnt', options=None, paths=None, foreground=True)
     mount_arguments = flexmock(mount_point='/mnt', options=None, paths=None, foreground=True)
@@ -312,6 +320,9 @@ def test_mount_archive_with_date_based_matching_calls_borg_with_date_based_flags
     )
     )
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         (
         (
             'borg',
             'borg',
@@ -330,9 +341,10 @@ def test_mount_archive_with_date_based_matching_calls_borg_with_date_based_flags
             'repo',
             'repo',
             '/mnt',
             '/mnt',
         ),
         ),
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     mount_arguments = flexmock(
     mount_arguments = flexmock(
@@ -353,3 +365,19 @@ def test_mount_archive_with_date_based_matching_calls_borg_with_date_based_flags
         local_borg_version='1.2.3',
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
         global_arguments=flexmock(log_json=False),
     )
     )
+
+
+def test_mount_archive_calls_borg_with_working_directory():
+    flexmock(module.feature).should_receive('available').and_return(False)
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    insert_execute_command_mock(('borg', 'mount', 'repo', '/mnt'), working_directory='/working/dir')
+
+    mount_arguments = flexmock(mount_point='/mnt', options=None, paths=None, foreground=False)
+    module.mount_archive(
+        repository_path='repo',
+        archive=None,
+        mount_arguments=mount_arguments,
+        config={'working_directory': '/working/dir'},
+        local_borg_version='1.2.3',
+        global_arguments=flexmock(log_json=False),
+    )

+ 36 - 4
tests/unit/borg/test_prune.py

@@ -7,14 +7,20 @@ from borgmatic.borg import prune as module
 from ..test_verbosity import insert_logging_mock
 from ..test_verbosity import insert_logging_mock
 
 
 
 
-def insert_execute_command_mock(prune_command, output_log_level, borg_exit_codes=None):
+def insert_execute_command_mock(
+    prune_command, output_log_level, working_directory=None, borg_exit_codes=None
+):
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        working_directory,
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         prune_command,
         prune_command,
         output_log_level=output_log_level,
         output_log_level=output_log_level,
+        extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=prune_command[0],
         borg_local_path=prune_command[0],
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     ).once()
     ).once()
 
 
 
 
@@ -235,7 +241,9 @@ def test_prune_archives_with_exit_codes_calls_borg_using_them():
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     borg_exit_codes = flexmock()
     borg_exit_codes = flexmock()
     insert_execute_command_mock(
     insert_execute_command_mock(
-        ('borg',) + PRUNE_COMMAND[1:] + ('repo',), logging.INFO, borg_exit_codes
+        ('borg',) + PRUNE_COMMAND[1:] + ('repo',),
+        logging.INFO,
+        borg_exit_codes=borg_exit_codes,
     )
     )
 
 
     prune_arguments = flexmock(stats=False, list_archives=False)
     prune_arguments = flexmock(stats=False, list_archives=False)
@@ -400,6 +408,9 @@ def test_prune_archives_with_date_based_matching_calls_borg_with_date_based_flag
     )
     )
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         (
         (
             'borg',
             'borg',
@@ -424,9 +435,10 @@ def test_prune_archives_with_date_based_matching_calls_borg_with_date_based_flag
             'repo',
             'repo',
         ),
         ),
         output_log_level=logging.INFO,
         output_log_level=logging.INFO,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     prune_arguments = flexmock(
     prune_arguments = flexmock(
@@ -440,3 +452,23 @@ def test_prune_archives_with_date_based_matching_calls_borg_with_date_based_flag
         global_arguments=flexmock(log_json=False),
         global_arguments=flexmock(log_json=False),
         prune_arguments=prune_arguments,
         prune_arguments=prune_arguments,
     )
     )
+
+
+def test_prune_archives_calls_borg_with_working_directory():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+    flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    insert_execute_command_mock(
+        PRUNE_COMMAND + ('repo',), logging.INFO, working_directory='/working/dir'
+    )
+
+    prune_arguments = flexmock(stats=False, list_archives=False)
+    module.prune_archives(
+        dry_run=False,
+        repository_path='repo',
+        config={'working_directory': '/working/dir'},
+        local_borg_version='1.2.3',
+        global_arguments=flexmock(log_json=False),
+        prune_arguments=prune_arguments,
+    )

+ 34 - 2
tests/unit/borg/test_repo_create.py

@@ -26,14 +26,20 @@ def insert_repo_info_command_not_found_mock():
     )
     )
 
 
 
 
-def insert_repo_create_command_mock(repo_create_command, borg_exit_codes=None, **kwargs):
+def insert_repo_create_command_mock(
+    repo_create_command, working_directory=None, borg_exit_codes=None, **kwargs
+):
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        working_directory,
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         repo_create_command,
         repo_create_command,
         output_file=module.DO_NOT_CAPTURE,
         output_file=module.DO_NOT_CAPTURE,
+        extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=repo_create_command[0],
         borg_local_path=repo_create_command[0],
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     ).once()
     ).once()
 
 
 
 
@@ -89,6 +95,9 @@ def test_create_repository_raises_for_borg_repo_create_error():
         )
         )
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').and_raise(
     flexmock(module).should_receive('execute_command').and_raise(
         module.subprocess.CalledProcessError(2, 'borg repo_create')
         module.subprocess.CalledProcessError(2, 'borg repo_create')
     )
     )
@@ -452,3 +461,26 @@ def test_create_repository_with_extra_borg_options_calls_borg_with_extra_options
         global_arguments=flexmock(log_json=False),
         global_arguments=flexmock(log_json=False),
         encryption_mode='repokey',
         encryption_mode='repokey',
     )
     )
+
+
+def test_create_repository_calls_borg_with_working_directory():
+    insert_repo_info_command_not_found_mock()
+    insert_repo_create_command_mock(
+        REPO_CREATE_COMMAND + ('--repo', 'repo'), working_directory='/working/dir'
+    )
+    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={'working_directory': '/working/dir'},
+        local_borg_version='2.3.4',
+        global_arguments=flexmock(log_json=False),
+        encryption_mode='repokey',
+    )

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

@@ -262,11 +262,15 @@ def test_delete_repository_with_defaults_does_not_capture_output():
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
         flexmock()
         flexmock()
     )
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
     flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
         command,
         command,
         output_log_level=module.logging.ANSWER,
         output_log_level=module.logging.ANSWER,
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
         extra_environment=object,
         extra_environment=object,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).once()
     ).once()
@@ -289,11 +293,15 @@ def test_delete_repository_with_force_captures_output():
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
         flexmock()
         flexmock()
     )
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
     flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
         command,
         command,
         output_log_level=module.logging.ANSWER,
         output_log_level=module.logging.ANSWER,
         output_file=None,
         output_file=None,
         extra_environment=object,
         extra_environment=object,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).once()
     ).once()
@@ -316,11 +324,15 @@ def test_delete_repository_with_cache_only_captures_output():
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
     flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
         flexmock()
         flexmock()
     )
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
     flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
         command,
         command,
         output_log_level=module.logging.ANSWER,
         output_log_level=module.logging.ANSWER,
         output_file=None,
         output_file=None,
         extra_environment=object,
         extra_environment=object,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).once()
     ).once()
@@ -334,3 +346,34 @@ def test_delete_repository_with_cache_only_captures_output():
         local_path='borg',
         local_path='borg',
         remote_path=None,
         remote_path=None,
     )
     )
+
+
+def test_delete_repository_calls_borg_with_working_directory():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    command = flexmock()
+    flexmock(module).should_receive('make_repo_delete_command').and_return(command)
+    flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
+        flexmock()
+    )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        '/working/dir',
+    )
+    flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
+        command,
+        output_log_level=module.logging.ANSWER,
+        output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
+        extra_environment=object,
+        working_directory='/working/dir',
+        borg_local_path='borg',
+        borg_exit_codes=None,
+    ).once()
+
+    module.delete_repository(
+        repository={'path': 'repo'},
+        config={'working_directory': '/working/dir'},
+        local_borg_version=flexmock(),
+        repo_delete_arguments=flexmock(force=False, cache_only=False),
+        global_arguments=flexmock(),
+        local_path='borg',
+        remote_path=None,
+    )

+ 110 - 13
tests/unit/borg/test_repo_info.py

@@ -18,19 +18,24 @@ def test_display_repository_info_calls_borg_with_flags():
         )
         )
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--json', '--repo', 'repo'),
         ('borg', 'repo-info', '--json', '--repo', 'repo'),
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     ).and_return('[]')
     ).and_return('[]')
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'repo-info', '--repo', 'repo'),
         ('borg', 'repo-info', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     module.display_repository_info(
     module.display_repository_info(
@@ -48,19 +53,24 @@ def test_display_repository_info_without_borg_features_calls_borg_with_info_sub_
     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_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--json', 'repo'),
         ('borg', 'repo-info', '--json', 'repo'),
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     ).and_return('[]')
     ).and_return('[]')
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'info', 'repo'),
         ('borg', 'info', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     module.display_repository_info(
     module.display_repository_info(
@@ -83,19 +93,24 @@ def test_display_repository_info_with_log_info_calls_borg_with_info_flag():
         )
         )
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--info', '--json', '--repo', 'repo'),
         ('borg', 'repo-info', '--info', '--json', '--repo', 'repo'),
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     ).and_return('[]')
     ).and_return('[]')
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'repo-info', '--info', '--repo', 'repo'),
         ('borg', 'repo-info', '--info', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
     insert_logging_mock(logging.INFO)
     insert_logging_mock(logging.INFO)
     module.display_repository_info(
     module.display_repository_info(
@@ -118,9 +133,13 @@ def test_display_repository_info_with_log_info_and_json_suppresses_most_borg_out
         )
         )
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--json', '--repo', 'repo'),
         ('borg', 'repo-info', '--json', '--repo', 'repo'),
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).and_return('[]')
     ).and_return('[]')
@@ -149,19 +168,24 @@ def test_display_repository_info_with_log_debug_calls_borg_with_debug_flag():
         )
         )
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--debug', '--show-rc', '--json', '--repo', 'repo'),
         ('borg', 'repo-info', '--debug', '--show-rc', '--json', '--repo', 'repo'),
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     ).and_return('[]')
     ).and_return('[]')
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'repo-info', '--debug', '--show-rc', '--repo', 'repo'),
         ('borg', 'repo-info', '--debug', '--show-rc', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
     insert_logging_mock(logging.DEBUG)
     insert_logging_mock(logging.DEBUG)
 
 
@@ -185,9 +209,13 @@ def test_display_repository_info_with_log_debug_and_json_suppresses_most_borg_ou
         )
         )
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--json', '--repo', 'repo'),
         ('borg', 'repo-info', '--json', '--repo', 'repo'),
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).and_return('[]')
     ).and_return('[]')
@@ -216,9 +244,13 @@ def test_display_repository_info_with_json_calls_borg_with_json_flag():
         )
         )
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--json', '--repo', 'repo'),
         ('borg', 'repo-info', '--json', '--repo', 'repo'),
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).and_return('[]')
     ).and_return('[]')
@@ -246,9 +278,13 @@ def test_display_repository_info_with_local_path_calls_borg_via_local_path():
         )
         )
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg1', 'repo-info', '--json', '--repo', 'repo'),
         ('borg1', 'repo-info', '--json', '--repo', 'repo'),
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).and_return('[]')
     ).and_return('[]')
@@ -256,9 +292,10 @@ def test_display_repository_info_with_local_path_calls_borg_via_local_path():
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg1', 'repo-info', '--repo', 'repo'),
         ('borg1', 'repo-info', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg1',
         borg_local_path='borg1',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     module.display_repository_info(
     module.display_repository_info(
@@ -282,10 +319,14 @@ def test_display_repository_info_with_exit_codes_calls_borg_using_them():
         )
         )
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     borg_exit_codes = flexmock()
     borg_exit_codes = flexmock()
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--json', '--repo', 'repo'),
         ('borg', 'repo-info', '--json', '--repo', 'repo'),
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
     ).and_return('[]')
     ).and_return('[]')
@@ -293,9 +334,10 @@ def test_display_repository_info_with_exit_codes_calls_borg_using_them():
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'repo-info', '--repo', 'repo'),
         ('borg', 'repo-info', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     )
     )
 
 
     module.display_repository_info(
     module.display_repository_info(
@@ -318,9 +360,13 @@ def test_display_repository_info_with_remote_path_calls_borg_with_remote_path_fl
         )
         )
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--remote-path', 'borg1', '--json', '--repo', 'repo'),
         ('borg', 'repo-info', '--remote-path', 'borg1', '--json', '--repo', 'repo'),
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).and_return('[]')
     ).and_return('[]')
@@ -328,9 +374,10 @@ def test_display_repository_info_with_remote_path_calls_borg_with_remote_path_fl
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'repo-info', '--remote-path', 'borg1', '--repo', 'repo'),
         ('borg', 'repo-info', '--remote-path', 'borg1', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     module.display_repository_info(
     module.display_repository_info(
@@ -354,9 +401,13 @@ def test_display_repository_info_with_log_json_calls_borg_with_log_json_flags():
         )
         )
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--log-json', '--json', '--repo', 'repo'),
         ('borg', 'repo-info', '--log-json', '--json', '--repo', 'repo'),
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).and_return('[]')
     ).and_return('[]')
@@ -364,9 +415,10 @@ def test_display_repository_info_with_log_json_calls_borg_with_log_json_flags():
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'repo-info', '--log-json', '--repo', 'repo'),
         ('borg', 'repo-info', '--log-json', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     module.display_repository_info(
     module.display_repository_info(
@@ -390,9 +442,13 @@ def test_display_repository_info_with_lock_wait_calls_borg_with_lock_wait_flags(
         )
         )
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--lock-wait', '5', '--json', '--repo', 'repo'),
         ('borg', 'repo-info', '--lock-wait', '5', '--json', '--repo', 'repo'),
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).and_return('[]')
     ).and_return('[]')
@@ -400,9 +456,10 @@ def test_display_repository_info_with_lock_wait_calls_borg_with_lock_wait_flags(
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'repo-info', '--lock-wait', '5', '--repo', 'repo'),
         ('borg', 'repo-info', '--lock-wait', '5', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     module.display_repository_info(
     module.display_repository_info(
@@ -412,3 +469,43 @@ def test_display_repository_info_with_lock_wait_calls_borg_with_lock_wait_flags(
         repo_info_arguments=flexmock(json=False),
         repo_info_arguments=flexmock(json=False),
         global_arguments=flexmock(log_json=False),
         global_arguments=flexmock(log_json=False),
     )
     )
+
+
+def test_display_repository_info_calls_borg_with_working_directory():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
+    flexmock(module.feature).should_receive('available').and_return(True)
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(
+        (
+            '--repo',
+            'repo',
+        )
+    )
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        '/working/dir',
+    )
+    flexmock(module).should_receive('execute_command_and_capture_output').with_args(
+        ('borg', 'repo-info', '--json', '--repo', 'repo'),
+        extra_environment=None,
+        working_directory='/working/dir',
+        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', '--repo', 'repo'),
+        output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory='/working/dir',
+        borg_local_path='borg',
+        borg_exit_codes=None,
+    )
+
+    module.display_repository_info(
+        repository_path='repo',
+        config={},
+        local_borg_version='2.3.4',
+        repo_info_arguments=flexmock(json=False),
+        global_arguments=flexmock(log_json=False),
+    )

+ 102 - 1
tests/unit/borg/test_repo_list.py

@@ -34,11 +34,15 @@ def test_resolve_archive_name_passes_through_non_latest_archive_name():
 def test_resolve_archive_name_calls_borg_with_flags():
 def test_resolve_archive_name_calls_borg_with_flags():
     expected_archive = 'archive-name'
     expected_archive = 'archive-name'
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
         ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
-        extra_environment=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
+        extra_environment=None,
+        working_directory=None,
     ).and_return(expected_archive + '\n')
     ).and_return(expected_archive + '\n')
 
 
     assert (
     assert (
@@ -56,9 +60,13 @@ def test_resolve_archive_name_calls_borg_with_flags():
 def test_resolve_archive_name_with_log_info_calls_borg_without_info_flag():
 def test_resolve_archive_name_with_log_info_calls_borg_without_info_flag():
     expected_archive = 'archive-name'
     expected_archive = 'archive-name'
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
         ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).and_return(expected_archive + '\n')
     ).and_return(expected_archive + '\n')
@@ -79,9 +87,13 @@ def test_resolve_archive_name_with_log_info_calls_borg_without_info_flag():
 def test_resolve_archive_name_with_log_debug_calls_borg_without_debug_flag():
 def test_resolve_archive_name_with_log_debug_calls_borg_without_debug_flag():
     expected_archive = 'archive-name'
     expected_archive = 'archive-name'
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
         ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).and_return(expected_archive + '\n')
     ).and_return(expected_archive + '\n')
@@ -102,9 +114,13 @@ def test_resolve_archive_name_with_log_debug_calls_borg_without_debug_flag():
 def test_resolve_archive_name_with_local_path_calls_borg_via_local_path():
 def test_resolve_archive_name_with_local_path_calls_borg_via_local_path():
     expected_archive = 'archive-name'
     expected_archive = 'archive-name'
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg1', 'list') + BORG_LIST_LATEST_ARGUMENTS,
         ('borg1', 'list') + BORG_LIST_LATEST_ARGUMENTS,
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg1',
         borg_local_path='borg1',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).and_return(expected_archive + '\n')
     ).and_return(expected_archive + '\n')
@@ -125,10 +141,14 @@ def test_resolve_archive_name_with_local_path_calls_borg_via_local_path():
 def test_resolve_archive_name_with_exit_codes_calls_borg_using_them():
 def test_resolve_archive_name_with_exit_codes_calls_borg_using_them():
     expected_archive = 'archive-name'
     expected_archive = 'archive-name'
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     borg_exit_codes = flexmock()
     borg_exit_codes = flexmock()
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
         ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
     ).and_return(expected_archive + '\n')
     ).and_return(expected_archive + '\n')
@@ -148,9 +168,13 @@ def test_resolve_archive_name_with_exit_codes_calls_borg_using_them():
 def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_flags():
 def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_flags():
     expected_archive = 'archive-name'
     expected_archive = 'archive-name'
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'list', '--remote-path', 'borg1') + BORG_LIST_LATEST_ARGUMENTS,
         ('borg', 'list', '--remote-path', 'borg1') + BORG_LIST_LATEST_ARGUMENTS,
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).and_return(expected_archive + '\n')
     ).and_return(expected_archive + '\n')
@@ -170,9 +194,13 @@ def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_flags
 
 
 def test_resolve_archive_name_without_archives_raises():
 def test_resolve_archive_name_without_archives_raises():
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
         ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).and_return('')
     ).and_return('')
@@ -191,9 +219,13 @@ def test_resolve_archive_name_with_log_json_calls_borg_with_log_json_flags():
     expected_archive = 'archive-name'
     expected_archive = 'archive-name'
 
 
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'list', '--log-json') + BORG_LIST_LATEST_ARGUMENTS,
         ('borg', 'list', '--log-json') + BORG_LIST_LATEST_ARGUMENTS,
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).and_return(expected_archive + '\n')
     ).and_return(expected_archive + '\n')
@@ -214,9 +246,13 @@ def test_resolve_archive_name_with_lock_wait_calls_borg_with_lock_wait_flags():
     expected_archive = 'archive-name'
     expected_archive = 'archive-name'
 
 
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'list', '--lock-wait', 'okay') + BORG_LIST_LATEST_ARGUMENTS,
         ('borg', 'list', '--lock-wait', 'okay') + BORG_LIST_LATEST_ARGUMENTS,
         extra_environment=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
     ).and_return(expected_archive + '\n')
     ).and_return(expected_archive + '\n')
@@ -233,6 +269,32 @@ def test_resolve_archive_name_with_lock_wait_calls_borg_with_lock_wait_flags():
     )
     )
 
 
 
 
+def test_resolve_archive_name_calls_borg_with_working_directory():
+    expected_archive = 'archive-name'
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        '/working/dir',
+    )
+    flexmock(module).should_receive('execute_command_and_capture_output').with_args(
+        ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
+        borg_local_path='borg',
+        borg_exit_codes=None,
+        extra_environment=None,
+        working_directory='/working/dir',
+    ).and_return(expected_archive + '\n')
+
+    assert (
+        module.resolve_archive_name(
+            'repo',
+            'latest',
+            config={'working_directory': '/working/dir'},
+            local_borg_version='1.2.3',
+            global_arguments=flexmock(log_json=False),
+        )
+        == expected_archive
+    )
+
+
 def test_make_repo_list_command_includes_log_info():
 def test_make_repo_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').and_return(())
@@ -594,6 +656,9 @@ def test_list_repository_calls_two_commands():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('make_repo_list_command')
     flexmock(module).should_receive('make_repo_list_command')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').once()
     flexmock(module).should_receive('execute_command_and_capture_output').once()
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
     flexmock(module).should_receive('execute_command').once()
     flexmock(module).should_receive('execute_command').once()
@@ -611,6 +676,9 @@ def test_list_repository_with_json_calls_json_command_only():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('make_repo_list_command')
     flexmock(module).should_receive('make_repo_list_command')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     json_output = flexmock()
     json_output = flexmock()
     flexmock(module).should_receive('execute_command_and_capture_output').and_return(json_output)
     flexmock(module).should_receive('execute_command_and_capture_output').and_return(json_output)
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never()
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never()
@@ -669,3 +737,36 @@ def test_make_repo_list_command_with_date_based_matching_calls_borg_with_date_ba
         '1w',
         '1w',
         'repo',
         'repo',
     )
     )
+
+
+def test_list_repository_calls_borg_with_working_directory():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module).should_receive('make_repo_list_command')
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        '/working/dir',
+    )
+    flexmock(module).should_receive('execute_command_and_capture_output').with_args(
+        full_command=object,
+        extra_environment=object,
+        working_directory='/working/dir',
+        borg_local_path=object,
+        borg_exit_codes=object,
+    ).once()
+    flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
+    flexmock(module).should_receive('execute_command').with_args(
+        full_command=object,
+        output_log_level=object,
+        extra_environment=object,
+        working_directory='/working/dir',
+        borg_local_path=object,
+        borg_exit_codes=object,
+    ).once()
+
+    module.list_repository(
+        repository_path='repo',
+        config={'working_directory': '/working/dir'},
+        local_borg_version='1.2.3',
+        repo_list_arguments=argparse.Namespace(json=False),
+        global_arguments=flexmock(),
+    )

+ 113 - 16
tests/unit/borg/test_transfer.py

@@ -16,13 +16,17 @@ def test_transfer_archives_calls_borg_with_flags():
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--repo', 'repo'),
         ('borg', 'transfer', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     module.transfer_archives(
     module.transfer_archives(
@@ -48,13 +52,17 @@ def test_transfer_archives_with_dry_run_calls_borg_with_dry_run_flag():
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--repo', 'repo', '--dry-run'),
         ('borg', 'transfer', '--repo', 'repo', '--dry-run'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     module.transfer_archives(
     module.transfer_archives(
@@ -77,13 +85,17 @@ def test_transfer_archives_with_log_info_calls_borg_with_info_flag():
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--info', '--repo', 'repo'),
         ('borg', 'transfer', '--info', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
     insert_logging_mock(logging.INFO)
     insert_logging_mock(logging.INFO)
     module.transfer_archives(
     module.transfer_archives(
@@ -106,13 +118,17 @@ def test_transfer_archives_with_log_debug_calls_borg_with_debug_flag():
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--debug', '--show-rc', '--repo', 'repo'),
         ('borg', 'transfer', '--debug', '--show-rc', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
     insert_logging_mock(logging.DEBUG)
     insert_logging_mock(logging.DEBUG)
 
 
@@ -138,13 +154,17 @@ def test_transfer_archives_with_archive_calls_borg_with_match_archives_flag():
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--match-archives', 'archive', '--repo', 'repo'),
         ('borg', 'transfer', '--match-archives', 'archive', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     module.transfer_archives(
     module.transfer_archives(
@@ -169,13 +189,17 @@ def test_transfer_archives_with_match_archives_calls_borg_with_match_archives_fl
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--match-archives', 'sh:foo*', '--repo', 'repo'),
         ('borg', 'transfer', '--match-archives', 'sh:foo*', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     module.transfer_archives(
     module.transfer_archives(
@@ -200,13 +224,17 @@ def test_transfer_archives_with_archive_name_format_calls_borg_with_match_archiv
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--match-archives', 'sh:bar-*', '--repo', 'repo'),
         ('borg', 'transfer', '--match-archives', 'sh:bar-*', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     module.transfer_archives(
     module.transfer_archives(
@@ -229,13 +257,17 @@ def test_transfer_archives_with_local_path_calls_borg_via_local_path():
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg2', 'transfer', '--repo', 'repo'),
         ('borg2', 'transfer', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg2',
         borg_local_path='borg2',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     module.transfer_archives(
     module.transfer_archives(
@@ -259,14 +291,18 @@ def test_transfer_archives_with_exit_codes_calls_borg_using_them():
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     borg_exit_codes = flexmock()
     borg_exit_codes = flexmock()
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--repo', 'repo'),
         ('borg', 'transfer', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     )
     )
 
 
     module.transfer_archives(
     module.transfer_archives(
@@ -292,13 +328,17 @@ def test_transfer_archives_with_remote_path_calls_borg_with_remote_path_flags():
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--remote-path', 'borg2', '--repo', 'repo'),
         ('borg', 'transfer', '--remote-path', 'borg2', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     module.transfer_archives(
     module.transfer_archives(
@@ -325,13 +365,17 @@ def test_transfer_archives_with_log_json_calls_borg_with_log_json_flags():
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--log-json', '--repo', 'repo'),
         ('borg', 'transfer', '--log-json', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     module.transfer_archives(
     module.transfer_archives(
@@ -358,13 +402,17 @@ def test_transfer_archives_with_lock_wait_calls_borg_with_lock_wait_flags():
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     config = {'lock_wait': 5}
     config = {'lock_wait': 5}
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--lock-wait', '5', '--repo', 'repo'),
         ('borg', 'transfer', '--lock-wait', '5', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     module.transfer_archives(
     module.transfer_archives(
@@ -387,13 +435,17 @@ def test_transfer_archives_with_progress_calls_borg_with_progress_flag():
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--progress',))
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--progress',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--progress', '--repo', 'repo'),
         ('borg', 'transfer', '--progress', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=module.DO_NOT_CAPTURE,
         output_file=module.DO_NOT_CAPTURE,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     module.transfer_archives(
     module.transfer_archives(
@@ -420,13 +472,17 @@ def test_transfer_archives_passes_through_arguments_to_borg(argument_name):
     )
     )
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', flag_name, 'value', '--repo', 'repo'),
         ('borg', 'transfer', flag_name, 'value', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     module.transfer_archives(
     module.transfer_archives(
@@ -455,13 +511,17 @@ def test_transfer_archives_with_source_repository_calls_borg_with_other_repo_fla
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--repo', 'repo', '--other-repo', 'other'),
         ('borg', 'transfer', '--repo', 'repo', '--other-repo', 'other'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     module.transfer_archives(
     module.transfer_archives(
@@ -485,6 +545,9 @@ def test_transfer_archives_with_date_based_matching_calls_borg_with_date_based_f
     )
     )
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         (
         (
             'borg',
             'borg',
@@ -502,9 +565,10 @@ def test_transfer_archives_with_date_based_matching_calls_borg_with_date_based_f
         ),
         ),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
         borg_exit_codes=None,
         borg_exit_codes=None,
-        extra_environment=None,
     )
     )
 
 
     module.transfer_archives(
     module.transfer_archives(
@@ -523,3 +587,36 @@ def test_transfer_archives_with_date_based_matching_calls_borg_with_date_based_f
             oldest='1w',
             oldest='1w',
         ),
         ),
     )
     )
+
+
+def test_transfer_archives_calls_borg_with_working_directory():
+    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'))
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        '/working/dir',
+    )
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'transfer', '--repo', 'repo'),
+        output_log_level=module.borgmatic.logger.ANSWER,
+        output_file=None,
+        extra_environment=None,
+        working_directory='/working/dir',
+        borg_local_path='borg',
+        borg_exit_codes=None,
+    )
+
+    module.transfer_archives(
+        dry_run=False,
+        repository_path='repo',
+        config={'working_directory': '/working/dir'},
+        local_borg_version='2.3.4',
+        transfer_arguments=flexmock(
+            archive=None, progress=None, match_archives=None, source_repository=None
+        ),
+        global_arguments=flexmock(log_json=False),
+    )

+ 16 - 2
tests/unit/borg/test_umount.py

@@ -7,9 +7,17 @@ from borgmatic.borg import umount as module
 from ..test_verbosity import insert_logging_mock
 from ..test_verbosity import insert_logging_mock
 
 
 
 
-def insert_execute_command_mock(command, borg_local_path='borg', borg_exit_codes=None):
+def insert_execute_command_mock(
+    command, borg_local_path='borg', working_directory=None, borg_exit_codes=None
+):
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        working_directory,
+    )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
-        command, borg_local_path=borg_local_path, borg_exit_codes=borg_exit_codes
+        command,
+        working_directory=working_directory,
+        borg_local_path=borg_local_path,
+        borg_exit_codes=borg_exit_codes,
     ).once()
     ).once()
 
 
 
 
@@ -44,3 +52,9 @@ def test_unmount_archive_calls_borg_with_exit_codes():
     insert_execute_command_mock(('borg', 'umount', '/mnt'), borg_exit_codes=borg_exit_codes)
     insert_execute_command_mock(('borg', 'umount', '/mnt'), borg_exit_codes=borg_exit_codes)
 
 
     module.unmount_archive(config={'borg_exit_codes': borg_exit_codes}, mount_point='/mnt')
     module.unmount_archive(config={'borg_exit_codes': borg_exit_codes}, mount_point='/mnt')
+
+
+def test_unmount_archive_calls_borg_with_working_directory():
+    insert_execute_command_mock(('borg', 'umount', '/mnt'), working_directory='/working/dir')
+
+    module.unmount_archive(config={'working_directory': '/working/dir'}, mount_point='/mnt')

+ 18 - 1
tests/unit/borg/test_version.py

@@ -11,12 +11,20 @@ VERSION = '1.2.3'
 
 
 
 
 def insert_execute_command_and_capture_output_mock(
 def insert_execute_command_and_capture_output_mock(
-    command, borg_local_path='borg', borg_exit_codes=None, version_output=f'borg {VERSION}'
+    command,
+    working_directory=None,
+    borg_local_path='borg',
+    borg_exit_codes=None,
+    version_output=f'borg {VERSION}',
 ):
 ):
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        working_directory,
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         command,
         command,
         extra_environment=None,
         extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=borg_local_path,
         borg_local_path=borg_local_path,
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
     ).once().and_return(version_output)
     ).once().and_return(version_output)
@@ -68,3 +76,12 @@ def test_local_borg_version_with_invalid_version_raises():
 
 
     with pytest.raises(ValueError):
     with pytest.raises(ValueError):
         module.local_borg_version({})
         module.local_borg_version({})
+
+
+def test_local_borg_version_calls_borg_with_working_directory():
+    insert_execute_command_and_capture_output_mock(
+        ('borg', '--version'), working_directory='/working/dir'
+    )
+    flexmock(module.environment).should_receive('make_environment')
+
+    assert module.local_borg_version({'working_directory': '/working/dir'}) == VERSION

+ 17 - 0
tests/unit/config/test_options.py

@@ -0,0 +1,17 @@
+from flexmock import flexmock
+
+from borgmatic.config import options as module
+
+
+def test_get_working_directory_passes_through_plain_directory():
+    flexmock(module.os.path).should_receive('expanduser').and_return('/home/foo')
+    assert module.get_working_directory({'working_directory': '/home/foo'}) == '/home/foo'
+
+
+def test_get_working_directory_expands_tildes():
+    flexmock(module.os.path).should_receive('expanduser').and_return('/home/foo')
+    assert module.get_working_directory({'working_directory': '~/foo'}) == '/home/foo'
+
+
+def test_get_working_directory_handles_no_configured_directory():
+    assert module.get_working_directory({}) is None