Przeglądaj źródła

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 miesięcy temu
rodzic
commit
bd4c672382
48 zmienionych plików z 1608 dodań i 178 usunięć
  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
+ * #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
    show a helpful error message.
  * #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.repo_list
 import borgmatic.borg.state
+import borgmatic.config.options
 import borgmatic.config.validate
 import borgmatic.execute
 import borgmatic.hooks.command
@@ -356,17 +357,13 @@ def collect_spot_check_source_paths(
         )
     )
     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(
         create_flags + create_positional_arguments,
         capture_stderr=True,
-        working_directory=working_directory,
         extra_environment=borg_environment,
+        working_directory=working_directory,
         borg_local_path=local_path,
         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('+ ')
     )
 
-    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'
@@ -444,8 +443,11 @@ def compare_spot_check_hashes(
         int(len(source_paths) * (min(spot_check_config['data_sample_percentage'], 100) / 100)), 1
     )
     source_sample_paths = tuple(random.sample(source_paths, sample_count))
+    working_directory = borgmatic.config.options.get_working_directory(config)
     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(
         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'),)
             + tuple(
                 path for path in source_sample_paths_subset if path in existing_source_sample_paths
-            )
+            ),
+            working_directory=working_directory,
         )
 
         source_hashes.update(

+ 2 - 0
borgmatic/borg/borg.py

@@ -2,6 +2,7 @@ import logging
 import shlex
 
 import borgmatic.commands.arguments
+import borgmatic.config.options
 import borgmatic.logger
 from borgmatic.borg import environment, flags
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
@@ -67,6 +68,7 @@ def run_arbitrary_borg(
                 'ARCHIVE': archive if archive else '',
             },
         ),
+        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/break_lock.py

@@ -1,5 +1,6 @@
 import logging
 
+import borgmatic.config.options
 from borgmatic.borg import environment, flags
 from borgmatic.execute import execute_command
 
@@ -37,6 +38,7 @@ def break_lock(
     execute_command(
         full_command,
         extra_environment=borg_environment,
+        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/change_passphrase.py

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

+ 5 - 0
borgmatic/borg/check.py

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

+ 2 - 0
borgmatic/borg/compact.py

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

+ 28 - 28
borgmatic/borg/create.py

@@ -7,6 +7,7 @@ import stat
 import tempfile
 import textwrap
 
+import borgmatic.config.options
 import borgmatic.logger
 from borgmatic.borg import environment, feature, flags, state
 from borgmatic.execute import (
@@ -19,26 +20,28 @@ from borgmatic.execute import (
 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
     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]
 
 
-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:
         return ()
 
     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)
 
 
-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
     the same device identifier).
     '''
     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 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
         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
     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):
         check_all_source_directories_exist(
@@ -371,11 +369,15 @@ def make_base_create_command(
             expand_directories(
                 tuple(config.get('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(
-            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.
     '''
     borgmatic.logger.add_custom_log_levels()
+
+    working_directory = borgmatic.config.options.get_working_directory(config)
     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) = (
@@ -555,11 +560,6 @@ def create_archive(
     # the terminal directly.
     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)
 
     create_flags += (

+ 2 - 0
borgmatic/borg/delete.py

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

+ 4 - 1
borgmatic/borg/export_key.py

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

+ 2 - 0
borgmatic/borg/export_tar.py

@@ -1,5 +1,6 @@
 import logging
 
+import borgmatic.config.options
 import borgmatic.logger
 from borgmatic.borg import environment, flags
 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_log_level=output_log_level,
         extra_environment=environment.make_environment(config),
+        working_directory=borgmatic.config.options.get_working_directory(config),
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
     )

+ 17 - 7
borgmatic/borg/extract.py

@@ -2,6 +2,7 @@ import logging
 import os
 import subprocess
 
+import borgmatic.config.options
 import borgmatic.config.validate
 from borgmatic.borg import environment, feature, flags, repo_list
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
@@ -58,8 +59,8 @@ def extract_last_archive_dry_run(
 
     execute_command(
         full_extract_command,
-        working_directory=None,
         extra_environment=borg_environment,
+        working_directory=borgmatic.config.options.get_working_directory(config),
         borg_local_path=local_path,
         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)
         )
 
+    working_directory = borgmatic.config.options.get_working_directory(config)
+
     full_command = (
         (local_path, 'extract')
         + (('--remote-path', remote_path) if remote_path else ())
@@ -126,9 +129,13 @@ def extract_archive(
         + (('--progress',) if progress else ())
         + (('--stdout',) if extract_to_stdout else ())
         + 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,
             local_borg_version,
         )
@@ -137,6 +144,9 @@ def extract_archive(
 
     borg_environment = environment.make_environment(config)
     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 terminal directly.
@@ -144,8 +154,8 @@ def extract_archive(
         return execute_command(
             full_command,
             output_file=DO_NOT_CAPTURE,
-            working_directory=destination_path,
             extra_environment=borg_environment,
+            working_directory=full_destination_path,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
         )
@@ -155,9 +165,9 @@ def extract_archive(
         return execute_command(
             full_command,
             output_file=subprocess.PIPE,
-            working_directory=destination_path,
             run_to_completion=False,
             extra_environment=borg_environment,
+            working_directory=full_destination_path,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
         )
@@ -166,8 +176,8 @@ def extract_archive(
     # if the restore paths don't exist in the archive.
     execute_command(
         full_command,
-        working_directory=destination_path,
         extra_environment=borg_environment,
+        working_directory=full_destination_path,
         borg_local_path=local_path,
         borg_exit_codes=borg_exit_codes,
     )

+ 4 - 0
borgmatic/borg/info.py

@@ -1,6 +1,7 @@
 import argparse
 import logging
 
+import borgmatic.config.options
 import borgmatic.logger
 from borgmatic.borg import environment, feature, flags
 from borgmatic.execute import execute_command, execute_command_and_capture_output
@@ -96,10 +97,12 @@ def display_archives_info(
         remote_path,
     )
     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_command,
         extra_environment=environment.make_environment(config),
+        working_directory=working_directory,
         borg_local_path=local_path,
         borg_exit_codes=borg_exit_codes,
     )
@@ -113,6 +116,7 @@ def display_archives_info(
         main_command,
         output_log_level=logging.ANSWER,
         extra_environment=environment.make_environment(config),
+        working_directory=working_directory,
         borg_local_path=local_path,
         borg_exit_codes=borg_exit_codes,
     )

+ 4 - 0
borgmatic/borg/list.py

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

+ 4 - 0
borgmatic/borg/mount.py

@@ -1,5 +1,6 @@
 import logging
 
+import borgmatic.config.options
 from borgmatic.borg import environment, feature, flags
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 
@@ -59,6 +60,7 @@ def mount_archive(
     )
 
     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.
     if mount_arguments.foreground:
@@ -66,6 +68,7 @@ def mount_archive(
             full_command,
             output_file=DO_NOT_CAPTURE,
             extra_environment=borg_environment,
+            working_directory=working_directory,
             borg_local_path=local_path,
             borg_exit_codes=config.get('borg_exit_codes'),
         )
@@ -74,6 +77,7 @@ def mount_archive(
     execute_command(
         full_command,
         extra_environment=borg_environment,
+        working_directory=working_directory,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
     )

+ 2 - 0
borgmatic/borg/prune.py

@@ -1,5 +1,6 @@
 import logging
 
+import borgmatic.config.options
 import borgmatic.logger
 from borgmatic.borg import environment, feature, flags
 from borgmatic.execute import execute_command
@@ -95,6 +96,7 @@ def prune_archives(
         full_command,
         output_log_level=output_log_level,
         extra_environment=environment.make_environment(config),
+        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/repo_create.py

@@ -3,6 +3,7 @@ import json
 import logging
 import subprocess
 
+import borgmatic.config.options
 from borgmatic.borg import environment, feature, flags, repo_info
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
 
@@ -96,6 +97,7 @@ def create_repository(
         repo_create_command,
         output_file=DO_NOT_CAPTURE,
         extra_environment=environment.make_environment(config),
+        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/repo_delete.py

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

+ 4 - 0
borgmatic/borg/repo_info.py

@@ -1,5 +1,6 @@
 import logging
 
+import borgmatic.config.options
 import borgmatic.logger
 from borgmatic.borg import environment, feature, flags
 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)
+    working_directory = borgmatic.config.options.get_working_directory(config)
     borg_exit_codes = config.get('borg_exit_codes')
 
     if repo_info_arguments.json:
         return execute_command_and_capture_output(
             full_command,
             extra_environment=extra_environment,
+            working_directory=working_directory,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
         )
@@ -63,6 +66,7 @@ def display_repository_info(
             full_command,
             output_log_level=logging.ANSWER,
             extra_environment=extra_environment,
+            working_directory=working_directory,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
         )

+ 5 - 0
borgmatic/borg/repo_list.py

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

+ 3 - 1
borgmatic/borg/transfer.py

@@ -1,5 +1,6 @@
 import logging
 
+import borgmatic.config.options
 import borgmatic.logger
 from borgmatic.borg import environment, flags
 from borgmatic.execute import DO_NOT_CAPTURE, execute_command
@@ -55,7 +56,8 @@ def transfer_archives(
         full_command,
         output_log_level=logging.ANSWER,
         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_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 borgmatic.config.options
 from borgmatic.execute import execute_command
 
 logger = logging.getLogger(__name__)
@@ -18,5 +19,8 @@ def unmount_archive(config, mount_point, local_path='borg'):
     )
 
     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 borgmatic.config.options
 from borgmatic.borg import environment
 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(
         full_command,
         extra_environment=environment.make_environment(config),
+        working_directory=borgmatic.config.options.get_working_directory(config),
         borg_local_path=local_path,
         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:
         type: string
         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.
         example: /path/to/working/directory
     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()
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive(
         'execute_command_and_capture_output'
     ).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()
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive(
         'execute_command_and_capture_output'
     ).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()
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive(
         'execute_command_and_capture_output'
     ).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()
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive(
         'execute_command_and_capture_output'
     ).and_return(
@@ -668,14 +680,60 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_source_dire
     ) == ('/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():
     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(
+        None,
+    )
     flexmock(module.os.path).should_receive('exists').and_return(True)
     flexmock(module.borgmatic.execute).should_receive(
         '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(
         ['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(
         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.borgmatic.execute).should_receive(
         '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(
         ['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(
         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.borgmatic.execute).should_receive(
         '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(
         ['hash1 /foo', 'nothash2 /bar']
     )
@@ -773,14 +841,19 @@ def test_compare_spot_check_hashes_uses_xxh64sum_command_option():
     ) == ('/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(
         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.borgmatic.execute).should_receive(
         '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(
         ['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(
         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('/bar').and_return(False)
     flexmock(module.borgmatic.execute).should_receive(
         '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(
         ['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(
         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.borgmatic.execute).should_receive(
         '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(
         '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(
         ['hash1 /foo', 'hash2 /bar']
     ).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',)
 
 
+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():
     with pytest.raises(ValueError):
         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.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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
 
     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.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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', '--info', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
     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.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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', '--debug', '--show-rc', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
     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')
     )
     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(
         ('borg', 'break-lock', '--lock-wait', '5', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
 
     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.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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', "'::$ARCHIVE'"),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': 'archive'},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
 
     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.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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg1', 'break-lock', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg1',
-        borg_exit_codes=None,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg1',
+        borg_exit_codes=None,
     )
 
     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.environment).should_receive('make_environment')
     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(
         ('borg', 'break-lock', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=borg_exit_codes,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=borg_exit_codes,
     )
 
     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')
     ).and_return(())
     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(
         ('borg', 'break-lock', '--remote-path', 'borg1', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
 
     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')
     ).and_return(())
     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(
         ('borg', 'break-lock', '--remote-path', "'borg1; naughty-command'", '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
 
     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.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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', '--progress', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
 
     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.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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'break-lock', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
 
     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.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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg',),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
 
     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.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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'key', 'export', '--info', '::'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
     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.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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'debug', 'dump-manifest', '--info', '::', 'path'),
         output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
-        borg_local_path='borg',
-        borg_exit_codes=None,
         shell=True,
         extra_environment={'BORG_REPO': 'repo', 'ARCHIVE': ''},
+        working_directory=None,
+        borg_local_path='borg',
+        borg_exit_codes=None,
     )
     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',
         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
 
 
-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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        working_directory,
+    )
     flexmock(module).should_receive('execute_command').with_args(
         command,
+        extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=command[0],
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     ).once()
 
 
@@ -128,3 +132,15 @@ def test_break_lock_with_log_debug_calls_borg_with_debug_flags():
         local_borg_version='1.2.3',
         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(
-    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()
 
     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(
         command,
         output_file=output_file,
         output_log_level=module.logging.ANSWER,
+        extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=command[0],
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     ).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():
     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()
 
     module.change_passphrase(
@@ -189,3 +200,21 @@ def test_change_passphrase_calls_borg_without_passphrase():
         change_passphrase_arguments=flexmock(),
         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
 
 
-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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        working_directory,
+    )
     flexmock(module).should_receive('execute_command').with_args(
         command,
         extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=command[0],
         borg_exit_codes=borg_exit_codes,
     ).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.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
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'check', '--progress', 'repo'),
         output_file=module.DO_NOT_CAPTURE,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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.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
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'check', '--repair', 'repo'),
         output_file=module.DO_NOT_CAPTURE,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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.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
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'check', '--max-duration', '33', 'repo'),
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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.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
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'check', '--max-duration', '33', 'repo'),
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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.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
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'check', '--max-duration', '44', 'repo'),
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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.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(
         ('borg', 'check', '--match-archives', 'foo-*', 'repo'),
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).once()
@@ -819,3 +847,31 @@ def test_check_archives_with_match_archives_passes_through_to_borg():
         checks={'archives'},
         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
 
 
-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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        working_directory
+    )
     flexmock(module).should_receive('execute_command').with_args(
         compact_command,
         output_log_level=output_log_level,
+        extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=compact_command[0],
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     ).once()
 
 
@@ -212,3 +218,18 @@ def test_compact_segments_with_extra_borg_options_calls_borg_with_extra_options(
         local_borg_version='1.2.3',
         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.glob).should_receive('glob').and_return([])
 
-    paths = module.expand_directory('foo')
+    paths = module.expand_directory('foo', None)
 
     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.glob).should_receive('glob').and_return(['foo', 'food'])
 
-    paths = module.expand_directory('foo*')
+    paths = module.expand_directory('foo*', None)
 
     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():
-    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']
     )
 
@@ -38,8 +60,18 @@ def test_expand_directories_flattens_expanded_directories():
     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():
-    paths = module.expand_directories(None)
+    paths = module.expand_directories(None, None)
 
     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():
+    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('/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():
+    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('/bar').and_raise(FileNotFoundError)
+    flexmock(module.os).should_receive('stat').with_args('/bar').never()
 
     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(
     '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():
+    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('map_directories_to_devices').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():
+    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('map_directories_to_devices').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():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('deduplicate_directories').and_return(
         ('foo', 'bar', '/tmp/test.yaml')
     )
     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(
-        ('foo', 'bar', '/tmp/test.yaml')
+        ('foo', 'bar', '/tmp/test.yaml'),
+        None,
     ).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.os.path).should_receive('expanduser').and_raise(TypeError)
     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():
+    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('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.os.path).should_receive('expanduser').and_raise(TypeError)
     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():
+    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('map_directories_to_devices').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(
     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('map_directories_to_devices').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():
+    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('map_directories_to_devices').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():
+    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('map_directories_to_devices').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():
+    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('map_directories_to_devices').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():
+    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('map_directories_to_devices').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():
+    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('map_directories_to_devices').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():
+    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('map_directories_to_devices').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():
+    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('map_directories_to_devices').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():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo*',))
     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():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'food'))
     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():
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -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():
+    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('map_directories_to_devices').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():
+    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)
 
     with pytest.raises(ValueError):
@@ -1302,6 +1408,9 @@ def test_create_archive_calls_borg_with_parameters():
         (('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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
@@ -1336,6 +1445,9 @@ def test_create_archive_calls_borg_with_environment():
     )
     environment = {'BORG_THINGY': 'YUP'}
     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(
         ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS,
         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())
     )
     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(
         ('borg', 'create', '--info') + REPO_ARCHIVE_WITH_PATHS,
         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())
     )
     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(
         ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
         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())
     )
     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(
         ('borg', 'create', '--debug', '--show-rc') + REPO_ARCHIVE_WITH_PATHS,
         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())
     )
     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(
         ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
         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())
     )
     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(
         ('borg', 'create', '--dry-run', '--info') + REPO_ARCHIVE_WITH_PATHS,
         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())
     )
     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,
@@ -1575,6 +1705,9 @@ def test_create_archive_with_exit_codes_calls_borg_using_them():
     )
     flexmock(module.environment).should_receive('make_environment')
     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(
         ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS,
         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())
     )
     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(
         ('borg', 'create', '--stats') + REPO_ARCHIVE_WITH_PATHS,
         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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'create', '--list', '--filter', 'FOO') + REPO_ARCHIVE_WITH_PATHS,
         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())
     )
     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(
         ('borg', 'create', '--info', '--progress') + REPO_ARCHIVE_WITH_PATHS,
         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())
     )
     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(
         ('borg', 'create', '--progress') + REPO_ARCHIVE_WITH_PATHS,
         output_log_level=logging.INFO,
@@ -1763,6 +1908,9 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr
         '--read-special',
         '--progress',
     ) + 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(
         create_command + ('--dry-run', '--list'),
         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())
     )
     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(
         ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
         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())
     )
     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(
         ('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
         working_directory=None,
@@ -1869,11 +2023,48 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_flag():
     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():
-    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')
     )
-    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',)
     )
     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():
-    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)
 
     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():
-    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').with_args('/tmp/foo').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()
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive('execute_command').once()
 
     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()
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive('execute_command').once()
 
     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()
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive('execute_command').once()
 
     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).should_receive('make_delete_command').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()
 
     module.delete_archives(
@@ -336,3 +348,33 @@ def test_delete_archives_without_archive_related_argument_calls_borg_repo_delete
         ),
         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
 
 
-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()
 
     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(
         command,
         output_file=output_file,
         output_log_level=module.logging.ANSWER,
+        extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=command[0],
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     ).once()
 
 
@@ -198,7 +204,7 @@ def test_export_key_calls_borg_with_qr_html_flag():
 
 def test_export_key_calls_borg_with_path_argument():
     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)
 
     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),
         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(
     command,
     output_log_level=logging.INFO,
+    working_directory=None,
     borg_local_path='borg',
     borg_exit_codes=None,
     capture=True,
 ):
     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(
         command,
         output_file=None if capture else module.DO_NOT_CAPTURE,
         output_log_level=output_log_level,
+        extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=borg_local_path,
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     ).once()
 
 
@@ -353,3 +358,26 @@ def test_export_tar_archive_calls_borg_with_stdout_destination_path():
         local_borg_version='1.2.3',
         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
 
 
-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).should_receive('execute_command').with_args(
         command,
-        working_directory=working_directory,
         extra_environment=None,
+        working_directory=destination_path,
         borg_local_path=command[0],
         borg_exit_codes=borg_exit_codes,
     ).once()
@@ -177,6 +177,9 @@ def test_extract_archive_calls_borg_with_path_flags():
     flexmock(module.os.path).should_receive('abspath').and_return('repo')
     insert_execute_command_mock(('borg', 'extract', 'repo::archive', 'path1', 'path2'))
     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(
         ('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')
     insert_execute_command_mock(('borg1', 'extract', 'repo::archive'))
     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(
         ('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
     )
     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(
         ('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')
     insert_execute_command_mock(('borg', 'extract', '--remote-path', 'borg1', 'repo::archive'))
     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(
         ('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')
     insert_execute_command_mock(('borg', 'extract', option_flag, 'repo::archive'))
     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(
         ('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')
     insert_execute_command_mock(('borg', 'extract', '--umask', '0770', 'repo::archive'))
     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(
         ('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')
     insert_execute_command_mock(('borg', 'extract', '--log-json', 'repo::archive'))
     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(
         ('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')
     insert_execute_command_mock(('borg', 'extract', '--lock-wait', '5', 'repo::archive'))
     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(
         ('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_logging_mock(logging.INFO)
     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(
         ('repo::archive',)
     )
@@ -388,6 +415,9 @@ def test_extract_archive_with_log_debug_calls_borg_with_debug_flags():
     )
     insert_logging_mock(logging.DEBUG)
     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(
         ('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')
     insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive'))
     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(
         ('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():
     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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('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')
     insert_execute_command_mock(('borg', 'extract', '--strip-components', '5', 'repo::archive'))
     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(
         ('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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
         ('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():
     flexmock(module.os.path).should_receive('abspath').and_return('repo')
     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(
         ('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():
     flexmock(module.os.path).should_receive('abspath').and_return('repo')
     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(
         ('borg', 'extract', '--progress', 'repo::archive'),
         output_file=module.DO_NOT_CAPTURE,
-        working_directory=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).once()
     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(
         ('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')
     process = flexmock()
     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(
         ('borg', 'extract', '--stdout', 'repo::archive'),
         output_file=module.subprocess.PIPE,
-        working_directory=None,
         run_to_completion=False,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).and_return(process).once()
     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(
         ('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():
     flexmock(module.os.path).should_receive('abspath').never()
     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(
         ('borg', 'extract', 'server:repo::archive'),
-        working_directory=None,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).once()
     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(
         ('server:repo::archive',)
     )
@@ -675,3 +741,50 @@ def test_extract_archive_skips_abspath_for_remote_repository():
         local_borg_version='1.2.3',
         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).should_receive('make_info_command')
     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.flags).should_receive('warn_for_aggressive_archive_flags')
     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).should_receive('make_info_command')
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     json_output = flexmock()
     flexmock(module).should_receive('execute_command_and_capture_output').and_return(json_output)
     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
     )
+
+
+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():
     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('make_list_command')
 
@@ -328,12 +331,16 @@ def test_list_archive_calls_borg_with_flags():
     ).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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', 'repo::archive'),
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     ).once()
 
     module.list_archive(
@@ -392,12 +399,16 @@ def test_list_archive_calls_borg_with_local_path():
     ).and_return(('borg2', '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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg2', 'list', 'repo::archive'),
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg2',
         borg_exit_codes=None,
-        extra_environment=None,
     ).once()
 
     module.list_archive(
@@ -440,12 +451,16 @@ def test_list_archive_calls_borg_using_exit_codes():
     ).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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', 'repo::archive'),
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     ).once()
 
     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(
         ('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(
         ('borg', 'list', 'repo'),
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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(
         ('borg', 'list', 'repo::archive1') + glob_paths,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     ).once()
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', 'repo::archive2') + glob_paths,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     ).once()
 
     module.list_archive(
@@ -541,12 +562,16 @@ def test_list_archive_calls_borg_with_archive():
     ).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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', 'repo::archive'),
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     ).once()
 
     module.list_archive(
@@ -662,12 +687,16 @@ def test_list_archive_with_archive_ignores_archive_filter_flag(
     ).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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', 'repo::archive'),
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     ).once()
 
     module.list_archive(
@@ -721,9 +750,13 @@ def test_list_archive_with_find_paths_allows_archive_filter_flag_but_only_passes
         remote_path=None,
     ).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(
         ('borg', 'repo-list', '--repo', 'repo'),
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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(
         ('borg', 'list', '--repo', 'repo', 'archive1') + glob_paths,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     ).once()
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'list', '--repo', 'repo', 'archive2') + glob_paths,
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     ).once()
 
     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,
     )
+
+
+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
 
 
-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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        working_directory,
+    )
     flexmock(module).should_receive('execute_command').with_args(
         command,
+        extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=command[0],
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     ).once()
 
 
@@ -254,12 +258,16 @@ def test_mount_archive_calls_borg_with_foreground_parameter():
         ('repo::archive',)
     )
     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(
         ('borg', 'mount', '--foreground', 'repo::archive', '/mnt'),
         output_file=module.DO_NOT_CAPTURE,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     ).once()
 
     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.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(
         (
             'borg',
@@ -330,9 +341,10 @@ def test_mount_archive_with_date_based_matching_calls_borg_with_date_based_flags
             'repo',
             '/mnt',
         ),
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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',
         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
 
 
-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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        working_directory,
+    )
     flexmock(module).should_receive('execute_command').with_args(
         prune_command,
         output_log_level=output_log_level,
+        extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=prune_command[0],
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     ).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',))
     borg_exit_codes = flexmock()
     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)
@@ -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.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(
         (
             'borg',
@@ -424,9 +435,10 @@ def test_prune_archives_with_date_based_matching_calls_borg_with_date_based_flag
             'repo',
         ),
         output_log_level=logging.INFO,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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),
         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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        working_directory,
+    )
     flexmock(module).should_receive('execute_command').with_args(
         repo_create_command,
         output_file=module.DO_NOT_CAPTURE,
+        extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=repo_create_command[0],
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     ).once()
 
 
@@ -89,6 +95,9 @@ def test_create_repository_raises_for_borg_repo_create_error():
         )
     )
     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(
         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),
         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()
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     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=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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()
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
         command,
         output_log_level=module.logging.ANSWER,
         output_file=None,
         extra_environment=object,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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()
     )
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
         command,
         output_log_level=module.logging.ANSWER,
         output_file=None,
         extra_environment=object,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).once()
@@ -334,3 +346,34 @@ def test_delete_repository_with_cache_only_captures_output():
         local_path='borg',
         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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--json', '--repo', 'repo'),
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=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=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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.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
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--json', 'repo'),
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     ).and_return('[]')
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'info', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--info', '--json', '--repo', 'repo'),
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     ).and_return('[]')
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'repo-info', '--info', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
     insert_logging_mock(logging.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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--json', '--repo', 'repo'),
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--debug', '--show-rc', '--json', '--repo', 'repo'),
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     ).and_return('[]')
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'repo-info', '--debug', '--show-rc', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
     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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--json', '--repo', 'repo'),
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--json', '--repo', 'repo'),
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg1', 'repo-info', '--json', '--repo', 'repo'),
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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(
         ('borg1', 'repo-info', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg1',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     borg_exit_codes = flexmock()
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--json', '--repo', 'repo'),
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=borg_exit_codes,
     ).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(
         ('borg', 'repo-info', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     )
 
     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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--remote-path', 'borg1', '--json', '--repo', 'repo'),
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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(
         ('borg', 'repo-info', '--remote-path', 'borg1', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--log-json', '--json', '--repo', 'repo'),
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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(
         ('borg', 'repo-info', '--log-json', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'repo-info', '--lock-wait', '5', '--json', '--repo', 'repo'),
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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(
         ('borg', 'repo-info', '--lock-wait', '5', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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),
         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():
     expected_archive = 'archive-name'
     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(
         ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
-        extra_environment=None,
         borg_local_path='borg',
         borg_exit_codes=None,
+        extra_environment=None,
+        working_directory=None,
     ).and_return(expected_archive + '\n')
 
     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():
     expected_archive = 'archive-name'
     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(
         ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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():
     expected_archive = 'archive-name'
     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(
         ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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():
     expected_archive = 'archive-name'
     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(
         ('borg1', 'list') + BORG_LIST_LATEST_ARGUMENTS,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg1',
         borg_exit_codes=None,
     ).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():
     expected_archive = 'archive-name'
     flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        None
+    )
     borg_exit_codes = flexmock()
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=borg_exit_codes,
     ).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():
     expected_archive = 'archive-name'
     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(
         ('borg', 'list', '--remote-path', 'borg1') + BORG_LIST_LATEST_ARGUMENTS,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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():
     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(
         ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).and_return('')
@@ -191,9 +219,13 @@ def test_resolve_archive_name_with_log_json_calls_borg_with_log_json_flags():
     expected_archive = 'archive-name'
 
     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(
         ('borg', 'list', '--log-json') + BORG_LIST_LATEST_ARGUMENTS,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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'
 
     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(
         ('borg', 'list', '--lock-wait', 'okay') + BORG_LIST_LATEST_ARGUMENTS,
         extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
     ).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():
     insert_logging_mock(logging.INFO)
     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).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(
+        None
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').once()
     flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
     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).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(
+        None
+    )
     json_output = flexmock()
     flexmock(module).should_receive('execute_command_and_capture_output').and_return(json_output)
     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',
         '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_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(
+        None
+    )
     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=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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_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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--repo', 'repo', '--dry-run'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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_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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--info', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
     insert_logging_mock(logging.INFO)
     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_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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--debug', '--show-rc', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
     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_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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--match-archives', 'archive', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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_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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--match-archives', 'sh:foo*', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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_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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--match-archives', 'sh:bar-*', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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_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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg2', 'transfer', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg2',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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_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(
+        None
+    )
     borg_exit_codes = flexmock()
     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=None,
         borg_local_path='borg',
         borg_exit_codes=borg_exit_codes,
-        extra_environment=None,
     )
 
     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_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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--remote-path', 'borg2', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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_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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--log-json', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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'))
     config = {'lock_wait': 5}
     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(
         ('borg', 'transfer', '--lock-wait', '5', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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_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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--progress', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=module.DO_NOT_CAPTURE,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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.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(
         ('borg', 'transfer', flag_name, 'value', '--repo', 'repo'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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_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(
+        None
+    )
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'transfer', '--repo', 'repo', '--other-repo', 'other'),
         output_log_level=module.borgmatic.logger.ANSWER,
         output_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     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.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(
         (
             '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_file=None,
+        extra_environment=None,
+        working_directory=None,
         borg_local_path='borg',
         borg_exit_codes=None,
-        extra_environment=None,
     )
 
     module.transfer_archives(
@@ -523,3 +587,36 @@ def test_transfer_archives_with_date_based_matching_calls_borg_with_date_based_f
             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
 
 
-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(
-        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()
 
 
@@ -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)
 
     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(
-    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.borgmatic.config.options).should_receive('get_working_directory').and_return(
+        working_directory,
+    )
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         command,
         extra_environment=None,
+        working_directory=working_directory,
         borg_local_path=borg_local_path,
         borg_exit_codes=borg_exit_codes,
     ).once().and_return(version_output)
@@ -68,3 +76,12 @@ def test_local_borg_version_with_invalid_version_raises():
 
     with pytest.raises(ValueError):
         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