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

Store included configuration files within each backup archive in support of the "config bootstrap" action (#736).

Dan Helfman 1 жил өмнө
parent
commit
63198088c4

+ 2 - 0
NEWS

@@ -1,4 +1,6 @@
 1.8.7.dev0
+ * #736: Store included configuration files within each backup archive in support of the "config
+   bootstrap" action. Previously, only top-level configuration files were stored.
  * #810: SECURITY: Prevent shell injection attacks within the PostgreSQL hook, the MongoDB hook, the
    SQLite hook, the "borgmatic borg" action, and command hook variable/constant interpolation.
 

+ 7 - 9
borgmatic/actions/config/bootstrap.py

@@ -13,14 +13,11 @@ logger = logging.getLogger(__name__)
 
 def get_config_paths(bootstrap_arguments, global_arguments, local_borg_version):
     '''
-    Given:
-    The bootstrap arguments, which include the repository and archive name, borgmatic source directory,
-    destination directory, and whether to strip components.
-    The global arguments, which include the dry run flag
-    and the local borg version,
-    Return:
-    The config paths from the manifest.json file in the borgmatic source directory after extracting it from the
-    repository.
+    Given the bootstrap arguments as an argparse.Namespace (containing the repository and archive
+    name, borgmatic source directory, destination directory, and whether to strip components), the
+    global arguments as an argparse.Namespace (containing the dry run flag and the local borg
+    version), return the config paths from the manifest.json file in the borgmatic source directory
+    after extracting it from the repository.
 
     Raise ValueError if the manifest JSON is missing, can't be decoded, or doesn't contain the
     expected configuration path data.
@@ -32,6 +29,7 @@ def get_config_paths(bootstrap_arguments, global_arguments, local_borg_version):
         os.path.join(borgmatic_source_directory, 'bootstrap', 'manifest.json')
     )
     config = {'ssh_command': bootstrap_arguments.ssh_command}
+
     extract_process = borgmatic.borg.extract.extract_archive(
         global_arguments.dry_run,
         bootstrap_arguments.repository,
@@ -48,8 +46,8 @@ def get_config_paths(bootstrap_arguments, global_arguments, local_borg_version):
         global_arguments,
         extract_to_stdout=True,
     )
-
     manifest_json = extract_process.stdout.read()
+
     if not manifest_json:
         raise ValueError(
             'Cannot read configuration paths from archive due to missing bootstrap manifest'

+ 5 - 1
borgmatic/actions/create.py

@@ -47,6 +47,7 @@ def run_create(
     config_filename,
     repository,
     config,
+    config_paths,
     hook_context,
     local_borg_version,
     create_arguments,
@@ -90,7 +91,9 @@ def run_create(
     )
     if config.get('store_config_files', True):
         create_borgmatic_manifest(
-            config, global_arguments.used_config_paths, global_arguments.dry_run
+            config,
+            config_paths,
+            global_arguments.dry_run,
         )
     stream_processes = [process for processes in active_dumps.values() for process in processes]
 
@@ -98,6 +101,7 @@ def run_create(
         global_arguments.dry_run,
         repository['path'],
         config,
+        config_paths,
         local_borg_version,
         global_arguments,
         local_path=local_path,

+ 5 - 7
borgmatic/borg/create.py

@@ -323,6 +323,7 @@ def create_archive(
     dry_run,
     repository_path,
     config,
+    config_paths,
     local_borg_version,
     global_arguments,
     local_path='borg',
@@ -334,8 +335,9 @@ def create_archive(
     stream_processes=None,
 ):
     '''
-    Given vebosity/dry-run flags, a local or remote repository path, and a configuration dict,
-    create a Borg archive and return Borg's JSON output (if any).
+    Given vebosity/dry-run flags, a local or remote repository path, a configuration dict, a
+    sequence of loaded configuration paths, the local Borg version, and global arguments as an
+    argparse.Namespace instance, create a Borg archive and return Borg's JSON output (if any).
 
     If a sequence of stream processes is given (instances of subprocess.Popen), then execute the
     create command while also triggering the given processes to produce output.
@@ -351,11 +353,7 @@ def create_archive(
             expand_directories(
                 tuple(config.get('source_directories', ()))
                 + borgmatic_source_directories
-                + tuple(
-                    global_arguments.used_config_paths
-                    if config.get('store_config_files', True)
-                    else ()
-                )
+                + tuple(config_paths if config.get('store_config_files', True) else ())
             )
         ),
         additional_directory_devices=map_directories_to_devices(

+ 24 - 19
borgmatic/commands/borgmatic.py

@@ -58,11 +58,11 @@ def get_skip_actions(config, arguments):
     return skip_actions
 
 
-def run_configuration(config_filename, config, arguments):
+def run_configuration(config_filename, config, config_paths, arguments):
     '''
-    Given a config filename, the corresponding parsed config dict, and command-line arguments as a
-    dict from subparser name to a namespace of parsed arguments, execute the defined create, prune,
-    compact, check, and/or other actions.
+    Given a config filename, the corresponding parsed config dict, a sequence of loaded
+    configuration paths, and command-line arguments as a dict from subparser name to a namespace of
+    parsed arguments, execute the defined create, prune, compact, check, and/or other actions.
 
     Yield a combination of:
 
@@ -144,6 +144,7 @@ def run_configuration(config_filename, config, arguments):
                     arguments=arguments,
                     config_filename=config_filename,
                     config=config,
+                    config_paths=config_paths,
                     local_path=local_path,
                     remote_path=remote_path,
                     local_borg_version=local_borg_version,
@@ -264,6 +265,7 @@ def run_actions(
     arguments,
     config_filename,
     config,
+    config_paths,
     local_path,
     remote_path,
     local_borg_version,
@@ -271,9 +273,9 @@ def run_actions(
 ):
     '''
     Given parsed command-line arguments as an argparse.ArgumentParser instance, the configuration
-    filename, several different configuration dicts, local and remote paths to Borg, a local Borg
-    version string, and a repository name, run all actions from the command-line arguments on the
-    given repository.
+    filename, a configuration dict, a sequence of loaded configuration paths, local and remote paths
+    to Borg, a local Borg version string, and a repository name, run all actions from the
+    command-line arguments on the given repository.
 
     Yield JSON output strings from executing any actions that produce JSON.
 
@@ -328,6 +330,7 @@ def run_actions(
                 config_filename,
                 repository,
                 config,
+                config_paths,
                 hook_context,
                 local_borg_version,
                 action_arguments,
@@ -502,13 +505,15 @@ def load_configurations(config_filenames, overrides=None, resolve_env=True):
     '''
     Given a sequence of configuration filenames, load and validate each configuration file. Return
     the results as a tuple of: dict of configuration filename to corresponding parsed configuration,
-    and sequence of logging.LogRecord instances containing any parse errors.
+    a sequence of paths for all loaded configuration files (including includes), and a sequence of
+    logging.LogRecord instances containing any parse errors.
 
     Log records are returned here instead of being logged directly because logging isn't yet
     initialized at this point!
     '''
     # Dict mapping from config filename to corresponding parsed config dict.
     configs = collections.OrderedDict()
+    config_paths = set()
     logs = []
 
     # Parse and load each configuration file.
@@ -525,9 +530,10 @@ def load_configurations(config_filenames, overrides=None, resolve_env=True):
             ]
         )
         try:
-            configs[config_filename], parse_logs = validate.parse_configuration(
+            configs[config_filename], paths, parse_logs = validate.parse_configuration(
                 config_filename, validate.schema_filename(), overrides, resolve_env
             )
+            config_paths.update(paths)
             logs.extend(parse_logs)
         except PermissionError:
             logs.extend(
@@ -557,7 +563,7 @@ def load_configurations(config_filenames, overrides=None, resolve_env=True):
                 ]
             )
 
-    return (configs, logs)
+    return (configs, sorted(config_paths), logs)
 
 
 def log_record(suppress_log=False, **kwargs):
@@ -724,12 +730,12 @@ def collect_highlander_action_summary_logs(configs, arguments, configuration_par
         return
 
 
-def collect_configuration_run_summary_logs(configs, arguments):
+def collect_configuration_run_summary_logs(configs, config_paths, arguments):
     '''
-    Given a dict of configuration filename to corresponding parsed configuration and parsed
-    command-line arguments as a dict from subparser name to a parsed namespace of arguments, run
-    each configuration file and yield a series of logging.LogRecord instances containing summary
-    information about each run.
+    Given a dict of configuration filename to corresponding parsed configuration, a sequence of
+    loaded configuration paths, and parsed command-line arguments as a dict from subparser name to a
+    parsed namespace of arguments, run each configuration file and yield a series of
+    logging.LogRecord instances containing summary information about each run.
 
     As a side effect of running through these configuration files, output their JSON results, if
     any, to stdout.
@@ -774,7 +780,7 @@ def collect_configuration_run_summary_logs(configs, arguments):
     # Execute the actions corresponding to each configuration file.
     json_results = []
     for config_filename, config in configs.items():
-        results = list(run_configuration(config_filename, config, arguments))
+        results = list(run_configuration(config_filename, config, config_paths, arguments))
         error_logs = tuple(result for result in results if isinstance(result, logging.LogRecord))
 
         if error_logs:
@@ -855,8 +861,7 @@ def main(extra_summary_logs=[]):  # pragma: no cover
         sys.exit(0)
 
     config_filenames = tuple(collect.collect_config_filenames(global_arguments.config_paths))
-    global_arguments.used_config_paths = list(config_filenames)
-    configs, parse_logs = load_configurations(
+    configs, config_paths, parse_logs = load_configurations(
         config_filenames, global_arguments.overrides, global_arguments.resolve_env
     )
     configuration_parse_errors = (
@@ -893,7 +898,7 @@ def main(extra_summary_logs=[]):  # pragma: no cover
                     configs, arguments, configuration_parse_errors
                 )
             )
-            or list(collect_configuration_run_summary_logs(configs, arguments))
+            or list(collect_configuration_run_summary_logs(configs, config_paths, arguments))
         )
     )
     summary_logs_max_level = max(log.levelno for log in summary_logs)

+ 1 - 2
borgmatic/config/generate.py

@@ -225,8 +225,7 @@ def merge_source_configuration_into_destination(destination_config, source_confi
     favoring values from the source when there are collisions.
 
     The purpose of this is to upgrade configuration files from old versions of borgmatic by adding
-    new
-    configuration keys and comments.
+    new configuration keys and comments.
     '''
     if not source_config:
         return destination_config

+ 39 - 25
borgmatic/config/load.py

@@ -9,18 +9,18 @@ import ruamel.yaml
 logger = logging.getLogger(__name__)
 
 
-def probe_and_include_file(filename, include_directories):
+def probe_and_include_file(filename, include_directories, config_paths):
     '''
-    Given a filename to include and a list of include directories to search for matching files,
-    probe for the file, load it, and return the loaded configuration as a data structure of nested
-    dicts, lists, etc.
+    Given a filename to include, a list of include directories to search for matching files, and a
+    set of configuration paths, probe for the file, load it, and return the loaded configuration as
+    a data structure of nested dicts, lists, etc. Add the filename to the given configuration paths.
 
     Raise FileNotFoundError if the included file was not found.
     '''
     expanded_filename = os.path.expanduser(filename)
 
     if os.path.isabs(expanded_filename):
-        return load_configuration(expanded_filename)
+        return load_configuration(expanded_filename, config_paths)
 
     candidate_filenames = {
         os.path.join(directory, expanded_filename) for directory in include_directories
@@ -28,32 +28,33 @@ def probe_and_include_file(filename, include_directories):
 
     for candidate_filename in candidate_filenames:
         if os.path.exists(candidate_filename):
-            return load_configuration(candidate_filename)
+            return load_configuration(candidate_filename, config_paths)
 
     raise FileNotFoundError(
         f'Could not find include {filename} at {" or ".join(candidate_filenames)}'
     )
 
 
-def include_configuration(loader, filename_node, include_directory):
+def include_configuration(loader, filename_node, include_directory, config_paths):
     '''
     Given a ruamel.yaml.loader.Loader, a ruamel.yaml.nodes.ScalarNode containing the included
-    filename (or a list containing multiple such filenames), and an include directory path to search
-    for matching files, load the given YAML filenames (ignoring the given loader so we can use our
-    own) and return their contents as data structure of nested dicts, lists, etc. If the given
+    filename (or a list containing multiple such filenames), an include directory path to search for
+    matching files, and a set of configuration paths, load the given YAML filenames (ignoring the
+    given loader so we can use our own) and return their contents as data structure of nested dicts,
+    lists, etc. Add the names of included files to the given configuration paths. If the given
     filename node's value is a scalar string, then the return value will be a single value. But if
     the given node value is a list, then the return value will be a list of values, one per loaded
     configuration file.
 
-    If a filename is relative, probe for it within 1. the current working directory and 2. the given
-    include directory.
+    If a filename is relative, probe for it within: 1. the current working directory and 2. the
+    given include directory.
 
     Raise FileNotFoundError if an included file was not found.
     '''
     include_directories = [os.getcwd(), os.path.abspath(include_directory)]
 
     if isinstance(filename_node.value, str):
-        return probe_and_include_file(filename_node.value, include_directories)
+        return probe_and_include_file(filename_node.value, include_directories, config_paths)
 
     if (
         isinstance(filename_node.value, list)
@@ -63,7 +64,7 @@ def include_configuration(loader, filename_node, include_directory):
         # Reversing the values ensures the correct ordering if these includes are subsequently
         # merged together.
         return [
-            probe_and_include_file(node.value, include_directories)
+            probe_and_include_file(node.value, include_directories, config_paths)
             for node in reversed(filename_node.value)
         ]
 
@@ -109,11 +110,17 @@ class Include_constructor(ruamel.yaml.SafeConstructor):
     separate YAML configuration files. Example syntax: `option: !include common.yaml`
     '''
 
-    def __init__(self, preserve_quotes=None, loader=None, include_directory=None):
+    def __init__(
+        self, preserve_quotes=None, loader=None, include_directory=None, config_paths=None
+    ):
         super(Include_constructor, self).__init__(preserve_quotes, loader)
         self.add_constructor(
             '!include',
-            functools.partial(include_configuration, include_directory=include_directory),
+            functools.partial(
+                include_configuration,
+                include_directory=include_directory,
+                config_paths=config_paths,
+            ),
         )
 
         # These are catch-all error handlers for tags that don't get applied and removed by
@@ -155,26 +162,33 @@ class Include_constructor(ruamel.yaml.SafeConstructor):
         node.value = deep_merge_nodes(node.value)
 
 
-def load_configuration(filename):
+def load_configuration(filename, config_paths=None):
     '''
     Load the given configuration file and return its contents as a data structure of nested dicts
-    and lists.
+    and lists. Add the filename to the given configuration paths set, and also add any included
+    configuration filenames.
 
     Raise ruamel.yaml.error.YAMLError if something goes wrong parsing the YAML, or RecursionError
     if there are too many recursive includes.
     '''
+    if config_paths is None:
+        config_paths = set()
 
-    # Use an embedded derived class for the include constructor so as to capture the filename
-    # value. (functools.partial doesn't work for this use case because yaml.Constructor has to be
-    # an actual class.)
-    class Include_constructor_with_include_directory(Include_constructor):
+    # Use an embedded derived class for the include constructor so as to capture the include
+    # directory and configuration paths values. (functools.partial doesn't work for this use case
+    # because yaml.Constructor has to be an actual class.)
+    class Include_constructor_with_extras(Include_constructor):
         def __init__(self, preserve_quotes=None, loader=None):
-            super(Include_constructor_with_include_directory, self).__init__(
-                preserve_quotes, loader, include_directory=os.path.dirname(filename)
+            super(Include_constructor_with_extras, self).__init__(
+                preserve_quotes,
+                loader,
+                include_directory=os.path.dirname(filename),
+                config_paths=config_paths,
             )
 
     yaml = ruamel.yaml.YAML(typ='safe')
-    yaml.Constructor = Include_constructor_with_include_directory
+    yaml.Constructor = Include_constructor_with_extras
+    config_paths.add(filename)
 
     with open(filename) as file:
         return yaml.load(file.read())

+ 6 - 4
borgmatic/config/validate.py

@@ -97,14 +97,16 @@ def parse_configuration(config_filename, schema_filename, overrides=None, resolv
             'checks': ['repository', 'archives'],
         }
 
-    Also return a sequence of logging.LogRecord instances containing any warnings about the
-    configuration.
+    Also return a set of loaded configuration paths and a sequence of logging.LogRecord instances
+    containing any warnings about the configuration.
 
     Raise FileNotFoundError if the file does not exist, PermissionError if the user does not
     have permissions to read the file, or Validation_error if the config does not match the schema.
     '''
+    config_paths = set()
+
     try:
-        config = load.load_configuration(config_filename)
+        config = load.load_configuration(config_filename, config_paths)
         schema = load.load_configuration(schema_filename)
     except (ruamel.yaml.error.YAMLError, RecursionError) as error:
         raise Validation_error(config_filename, (str(error),))
@@ -130,7 +132,7 @@ def parse_configuration(config_filename, schema_filename, overrides=None, resolv
 
     apply_logical_validation(config_filename, config)
 
-    return config, logs
+    return config, config_paths, logs
 
 
 def normalize_repository_path(repository):

+ 5 - 0
docs/how-to/extract-a-backup.md

@@ -191,3 +191,8 @@ for bootstrapping.
 borgmatic configuration files, for instance if they contain sensitive
 information you don't want to store even inside your encrypted backups. If you
 do this though, the `config bootstrap` action will no longer work.
+
+<span class="minilink minilink-addedin">New in version 1.8.7</span> Included
+configuration files are stored in each backup archive. This means that the
+`config bootstrap` action not only extracts the top-level configuration files
+but also the includes they depend upon.

+ 64 - 21
tests/integration/config/test_load.py

@@ -12,7 +12,10 @@ def test_load_configuration_parses_contents():
     config_file = io.StringIO('key: value')
     config_file.name = 'config.yaml'
     builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
-    assert module.load_configuration('config.yaml') == {'key': 'value'}
+    config_paths = {'other.yaml'}
+
+    assert module.load_configuration('config.yaml', config_paths) == {'key': 'value'}
+    assert config_paths == {'config.yaml', 'other.yaml'}
 
 
 def test_load_configuration_with_only_integer_value_does_not_raise():
@@ -20,7 +23,10 @@ def test_load_configuration_with_only_integer_value_does_not_raise():
     config_file = io.StringIO('33')
     config_file.name = 'config.yaml'
     builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
-    assert module.load_configuration('config.yaml') == 33
+    config_paths = {'other.yaml'}
+
+    assert module.load_configuration('config.yaml', config_paths) == 33
+    assert config_paths == {'config.yaml', 'other.yaml'}
 
 
 def test_load_configuration_inlines_include_relative_to_current_directory():
@@ -34,8 +40,10 @@ def test_load_configuration_inlines_include_relative_to_current_directory():
     config_file = io.StringIO('key: !include include.yaml')
     config_file.name = 'config.yaml'
     builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
+    config_paths = {'other.yaml'}
 
-    assert module.load_configuration('config.yaml') == {'key': 'value'}
+    assert module.load_configuration('config.yaml', config_paths) == {'key': 'value'}
+    assert config_paths == {'config.yaml', '/tmp/include.yaml', 'other.yaml'}
 
 
 def test_load_configuration_inlines_include_relative_to_config_parent_directory():
@@ -56,8 +64,10 @@ def test_load_configuration_inlines_include_relative_to_config_parent_directory(
     config_file = io.StringIO('key: !include include.yaml')
     config_file.name = '/etc/config.yaml'
     builtins.should_receive('open').with_args('/etc/config.yaml').and_return(config_file)
+    config_paths = {'other.yaml'}
 
-    assert module.load_configuration('/etc/config.yaml') == {'key': 'value'}
+    assert module.load_configuration('/etc/config.yaml', config_paths) == {'key': 'value'}
+    assert config_paths == {'/etc/config.yaml', '/etc/include.yaml', 'other.yaml'}
 
 
 def test_load_configuration_raises_if_relative_include_does_not_exist():
@@ -70,9 +80,10 @@ def test_load_configuration_raises_if_relative_include_does_not_exist():
     config_file = io.StringIO('key: !include include.yaml')
     config_file.name = '/etc/config.yaml'
     builtins.should_receive('open').with_args('/etc/config.yaml').and_return(config_file)
+    config_paths = set()
 
     with pytest.raises(FileNotFoundError):
-        module.load_configuration('/etc/config.yaml')
+        module.load_configuration('/etc/config.yaml', config_paths)
 
 
 def test_load_configuration_inlines_absolute_include():
@@ -86,8 +97,10 @@ def test_load_configuration_inlines_absolute_include():
     config_file = io.StringIO('key: !include /root/include.yaml')
     config_file.name = 'config.yaml'
     builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
+    config_paths = {'other.yaml'}
 
-    assert module.load_configuration('config.yaml') == {'key': 'value'}
+    assert module.load_configuration('config.yaml', config_paths) == {'key': 'value'}
+    assert config_paths == {'config.yaml', '/root/include.yaml', 'other.yaml'}
 
 
 def test_load_configuration_raises_if_absolute_include_does_not_exist():
@@ -98,9 +111,10 @@ def test_load_configuration_raises_if_absolute_include_does_not_exist():
     config_file = io.StringIO('key: !include /root/include.yaml')
     config_file.name = 'config.yaml'
     builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
+    config_paths = set()
 
     with pytest.raises(FileNotFoundError):
-        assert module.load_configuration('config.yaml')
+        assert module.load_configuration('config.yaml', config_paths)
 
 
 def test_load_configuration_inlines_multiple_file_include_as_list():
@@ -117,8 +131,15 @@ def test_load_configuration_inlines_multiple_file_include_as_list():
     config_file = io.StringIO('key: !include [/root/include1.yaml, /root/include2.yaml]')
     config_file.name = 'config.yaml'
     builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
-
-    assert module.load_configuration('config.yaml') == {'key': ['value2', 'value1']}
+    config_paths = {'other.yaml'}
+
+    assert module.load_configuration('config.yaml', config_paths) == {'key': ['value2', 'value1']}
+    assert config_paths == {
+        'config.yaml',
+        '/root/include1.yaml',
+        '/root/include2.yaml',
+        'other.yaml',
+    }
 
 
 def test_load_configuration_include_with_unsupported_filename_type_raises():
@@ -129,9 +150,10 @@ def test_load_configuration_include_with_unsupported_filename_type_raises():
     config_file = io.StringIO('key: !include {path: /root/include.yaml}')
     config_file.name = 'config.yaml'
     builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
+    config_paths = set()
 
     with pytest.raises(ValueError):
-        module.load_configuration('config.yaml')
+        module.load_configuration('config.yaml', config_paths)
 
 
 def test_load_configuration_merges_include():
@@ -155,8 +177,13 @@ def test_load_configuration_merges_include():
     )
     config_file.name = 'config.yaml'
     builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
+    config_paths = {'other.yaml'}
 
-    assert module.load_configuration('config.yaml') == {'foo': 'override', 'baz': 'quux'}
+    assert module.load_configuration('config.yaml', config_paths) == {
+        'foo': 'override',
+        'baz': 'quux',
+    }
+    assert config_paths == {'config.yaml', '/tmp/include.yaml', 'other.yaml'}
 
 
 def test_load_configuration_merges_multiple_file_include():
@@ -188,12 +215,14 @@ def test_load_configuration_merges_multiple_file_include():
     )
     config_file.name = 'config.yaml'
     builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
+    config_paths = {'other.yaml'}
 
-    assert module.load_configuration('config.yaml') == {
+    assert module.load_configuration('config.yaml', config_paths) == {
         'foo': 'override',
         'baz': 'second',
         'original': 'yes',
     }
+    assert config_paths == {'config.yaml', '/tmp/include1.yaml', '/tmp/include2.yaml', 'other.yaml'}
 
 
 def test_load_configuration_with_retain_tag_merges_include_but_keeps_local_values():
@@ -226,11 +255,13 @@ def test_load_configuration_with_retain_tag_merges_include_but_keeps_local_value
     )
     config_file.name = 'config.yaml'
     builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
+    config_paths = {'other.yaml'}
 
-    assert module.load_configuration('config.yaml') == {
+    assert module.load_configuration('config.yaml', config_paths) == {
         'stuff': {'foo': 'override'},
         'other': {'a': 'override', 'c': 'd'},
     }
+    assert config_paths == {'config.yaml', '/tmp/include.yaml', 'other.yaml'}
 
 
 def test_load_configuration_with_retain_tag_but_without_merge_include_raises():
@@ -256,9 +287,10 @@ def test_load_configuration_with_retain_tag_but_without_merge_include_raises():
     )
     config_file.name = 'config.yaml'
     builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
+    config_paths = set()
 
     with pytest.raises(ValueError):
-        module.load_configuration('config.yaml')
+        module.load_configuration('config.yaml', config_paths)
 
 
 def test_load_configuration_with_retain_tag_on_scalar_raises():
@@ -284,9 +316,10 @@ def test_load_configuration_with_retain_tag_on_scalar_raises():
     )
     config_file.name = 'config.yaml'
     builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
+    config_paths = set()
 
     with pytest.raises(ValueError):
-        module.load_configuration('config.yaml')
+        module.load_configuration('config.yaml', config_paths)
 
 
 def test_load_configuration_with_omit_tag_merges_include_and_omits_requested_values():
@@ -315,8 +348,10 @@ def test_load_configuration_with_omit_tag_merges_include_and_omits_requested_val
     )
     config_file.name = 'config.yaml'
     builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
+    config_paths = {'other.yaml'}
 
-    assert module.load_configuration('config.yaml') == {'stuff': ['a', 'c', 'x', 'y']}
+    assert module.load_configuration('config.yaml', config_paths) == {'stuff': ['a', 'c', 'x', 'y']}
+    assert config_paths == {'config.yaml', '/tmp/include.yaml', 'other.yaml'}
 
 
 def test_load_configuration_with_omit_tag_on_unknown_value_merges_include_and_does_not_raise():
@@ -345,8 +380,12 @@ def test_load_configuration_with_omit_tag_on_unknown_value_merges_include_and_do
     )
     config_file.name = 'config.yaml'
     builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
+    config_paths = {'other.yaml'}
 
-    assert module.load_configuration('config.yaml') == {'stuff': ['a', 'b', 'c', 'x', 'y']}
+    assert module.load_configuration('config.yaml', config_paths) == {
+        'stuff': ['a', 'b', 'c', 'x', 'y']
+    }
+    assert config_paths == {'config.yaml', '/tmp/include.yaml', 'other.yaml'}
 
 
 def test_load_configuration_with_omit_tag_on_non_list_item_raises():
@@ -374,9 +413,10 @@ def test_load_configuration_with_omit_tag_on_non_list_item_raises():
     )
     config_file.name = 'config.yaml'
     builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
+    config_paths = set()
 
     with pytest.raises(ValueError):
-        module.load_configuration('config.yaml')
+        module.load_configuration('config.yaml', config_paths)
 
 
 def test_load_configuration_with_omit_tag_on_non_scalar_list_item_raises():
@@ -403,9 +443,10 @@ def test_load_configuration_with_omit_tag_on_non_scalar_list_item_raises():
     )
     config_file.name = 'config.yaml'
     builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
+    config_paths = set()
 
     with pytest.raises(ValueError):
-        module.load_configuration('config.yaml')
+        module.load_configuration('config.yaml', config_paths)
 
 
 def test_load_configuration_with_omit_tag_but_without_merge_raises():
@@ -433,9 +474,10 @@ def test_load_configuration_with_omit_tag_but_without_merge_raises():
     )
     config_file.name = 'config.yaml'
     builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
+    config_paths = set()
 
     with pytest.raises(ValueError):
-        module.load_configuration('config.yaml')
+        module.load_configuration('config.yaml', config_paths)
 
 
 def test_load_configuration_does_not_merge_include_list():
@@ -460,9 +502,10 @@ def test_load_configuration_does_not_merge_include_list():
     )
     config_file.name = 'config.yaml'
     builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
+    config_paths = set()
 
     with pytest.raises(module.ruamel.yaml.error.YAMLError):
-        assert module.load_configuration('config.yaml')
+        assert module.load_configuration('config.yaml', config_paths)
 
 
 @pytest.mark.parametrize(

+ 15 - 6
tests/integration/config/test_validate.py

@@ -58,7 +58,7 @@ def test_parse_configuration_transforms_file_into_mapping():
         '''
     )
 
-    config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
+    config, config_paths, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
 
     assert config == {
         'source_directories': ['/home', '/etc'],
@@ -68,6 +68,7 @@ def test_parse_configuration_transforms_file_into_mapping():
         'keep_minutely': 60,
         'checks': [{'name': 'repository'}, {'name': 'archives'}],
     }
+    assert config_paths == {'/tmp/config.yaml'}
     assert logs == []
 
 
@@ -84,12 +85,13 @@ def test_parse_configuration_passes_through_quoted_punctuation():
         '''
     )
 
-    config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
+    config, config_paths, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
 
     assert config == {
         'source_directories': [f'/home/{string.punctuation}'],
         'repositories': [{'path': 'test.borg'}],
     }
+    assert config_paths == {'/tmp/config.yaml'}
     assert logs == []
 
 
@@ -141,7 +143,7 @@ def test_parse_configuration_inlines_include_inside_deprecated_section():
     include_file.name = 'include.yaml'
     builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
 
-    config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
+    config, config_paths, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
 
     assert config == {
         'source_directories': ['/home'],
@@ -149,6 +151,7 @@ def test_parse_configuration_inlines_include_inside_deprecated_section():
         'keep_daily': 7,
         'keep_hourly': 24,
     }
+    assert config_paths == {'/tmp/include.yaml', '/tmp/config.yaml'}
     assert len(logs) == 1
 
 
@@ -175,7 +178,7 @@ def test_parse_configuration_merges_include():
     include_file.name = 'include.yaml'
     builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
 
-    config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
+    config, config_paths, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
 
     assert config == {
         'source_directories': ['/home'],
@@ -183,6 +186,7 @@ def test_parse_configuration_merges_include():
         'keep_daily': 1,
         'keep_hourly': 24,
     }
+    assert config_paths == {'/tmp/include.yaml', '/tmp/config.yaml'}
     assert logs == []
 
 
@@ -194,6 +198,9 @@ def test_parse_configuration_raises_for_missing_config_file():
 def test_parse_configuration_raises_for_missing_schema_file():
     mock_config_and_schema('')
     builtins = flexmock(sys.modules['builtins'])
+    builtins.should_receive('open').with_args('/tmp/config.yaml').and_return(
+        io.StringIO('foo: bar')
+    )
     builtins.should_receive('open').with_args('/tmp/schema.yaml').and_raise(FileNotFoundError)
 
     with pytest.raises(FileNotFoundError):
@@ -233,7 +240,7 @@ def test_parse_configuration_applies_overrides():
         '''
     )
 
-    config, logs = module.parse_configuration(
+    config, config_paths, logs = module.parse_configuration(
         '/tmp/config.yaml', '/tmp/schema.yaml', overrides=['location.local_path=borg2']
     )
 
@@ -242,6 +249,7 @@ def test_parse_configuration_applies_overrides():
         'repositories': [{'path': 'hostname.borg'}],
         'local_path': 'borg2',
     }
+    assert config_paths == {'/tmp/config.yaml'}
     assert logs == []
 
 
@@ -260,11 +268,12 @@ def test_parse_configuration_applies_normalization_after_environment_variable_in
     )
     flexmock(os).should_receive('getenv').replace_with(lambda variable_name, default: default)
 
-    config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
+    config, config_paths, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
 
     assert config == {
         'source_directories': ['/home'],
         'repositories': [{'path': 'ssh://user@hostname/./repo'}],
         'exclude_if_present': ['.nobackup'],
     }
+    assert config_paths == {'/tmp/config.yaml'}
     assert logs

+ 10 - 5
tests/unit/actions/test_create.py

@@ -22,13 +22,14 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository():
         json=False,
         list_files=flexmock(),
     )
-    global_arguments = flexmock(monitoring_verbosity=1, dry_run=False, used_config_paths=[])
+    global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
 
     list(
         module.run_create(
             config_filename='test.yaml',
             repository={'path': 'repo'},
             config={},
+            config_paths=['/tmp/test.yaml'],
             hook_context={},
             local_borg_version=None,
             create_arguments=create_arguments,
@@ -57,13 +58,14 @@ def test_run_create_with_store_config_files_false_does_not_create_borgmatic_mani
         json=False,
         list_files=flexmock(),
     )
-    global_arguments = flexmock(monitoring_verbosity=1, dry_run=False, used_config_paths=[])
+    global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
 
     list(
         module.run_create(
             config_filename='test.yaml',
             repository={'path': 'repo'},
             config={'store_config_files': False},
+            config_paths=['/tmp/test.yaml'],
             hook_context={},
             local_borg_version=None,
             create_arguments=create_arguments,
@@ -94,13 +96,14 @@ def test_run_create_runs_with_selected_repository():
         json=False,
         list_files=flexmock(),
     )
-    global_arguments = flexmock(monitoring_verbosity=1, dry_run=False, used_config_paths=[])
+    global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
 
     list(
         module.run_create(
             config_filename='test.yaml',
             repository={'path': 'repo'},
             config={},
+            config_paths=['/tmp/test.yaml'],
             hook_context={},
             local_borg_version=None,
             create_arguments=create_arguments,
@@ -126,13 +129,14 @@ def test_run_create_bails_if_repository_does_not_match():
         json=False,
         list_files=flexmock(),
     )
-    global_arguments = flexmock(monitoring_verbosity=1, dry_run=False, used_config_paths=[])
+    global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
 
     list(
         module.run_create(
             config_filename='test.yaml',
             repository='repo',
             config={},
+            config_paths=['/tmp/test.yaml'],
             hook_context={},
             local_borg_version=None,
             create_arguments=create_arguments,
@@ -167,13 +171,14 @@ def test_run_create_produces_json():
         json=True,
         list_files=flexmock(),
     )
-    global_arguments = flexmock(monitoring_verbosity=1, dry_run=False, used_config_paths=[])
+    global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
 
     assert list(
         module.run_create(
             config_filename='test.yaml',
             repository={'path': 'repo'},
             config={},
+            config_paths=['/tmp/test.yaml'],
             hook_context={},
             local_borg_version=None,
             create_arguments=create_arguments,

+ 104 - 55
tests/unit/borg/test_create.py

@@ -506,8 +506,9 @@ def test_create_archive_calls_borg_with_parameters():
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -549,8 +550,9 @@ def test_create_archive_calls_borg_with_environment():
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -594,23 +596,24 @@ def test_create_archive_with_patterns_calls_borg_with_patterns_including_convert
             'repositories': ['repo'],
             'patterns': ['pattern'],
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
-def test_create_archive_with_sources_and_used_config_paths_calls_borg_with_sources_and_config_paths():
+def test_create_archive_with_sources_and_config_paths_calls_borg_with_sources_and_config_paths():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(
-        ('foo', 'bar', '/etc/borgmatic/config.yaml')
+        ('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(
-        ('foo', 'bar', '/etc/borgmatic/config.yaml')
-    ).and_return(('foo', 'bar', '/etc/borgmatic/config.yaml'))
+        ('foo', 'bar', '/tmp/test.yaml')
+    ).and_return(('foo', 'bar', '/tmp/test.yaml'))
     flexmock(module).should_receive('expand_directories').with_args([]).and_return(())
     flexmock(module).should_receive('pattern_root_directories').and_return([])
     flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
@@ -627,7 +630,7 @@ def test_create_archive_with_sources_and_used_config_paths_calls_borg_with_sourc
     environment = {'BORG_THINGY': 'YUP'}
     flexmock(module.environment).should_receive('make_environment').and_return(environment)
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('/etc/borgmatic/config.yaml',),
+        ('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('/tmp/test.yaml',),
         output_log_level=logging.INFO,
         output_file=None,
         borg_local_path='borg',
@@ -642,12 +645,13 @@ def test_create_archive_with_sources_and_used_config_paths_calls_borg_with_sourc
             'source_directories': ['foo', 'bar'],
             'repositories': ['repo'],
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=['/etc/borgmatic/config.yaml']),
+        global_arguments=flexmock(log_json=False),
     )
 
 
-def test_create_archive_with_sources_and_used_config_paths_with_store_config_files_false_calls_borg_with_sources_and_no_config_paths():
+def test_create_archive_with_sources_and_config_paths_with_store_config_files_false_calls_borg_with_sources_and_no_config_paths():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
@@ -689,8 +693,9 @@ def test_create_archive_with_sources_and_used_config_paths_with_store_config_fil
             'repositories': ['repo'],
             'store_config_files': False,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=['/etc/borgmatic/config.yaml']),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -734,8 +739,9 @@ def test_create_archive_with_exclude_patterns_calls_borg_with_excludes():
             'repositories': ['repo'],
             'exclude_patterns': ['exclude'],
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -777,8 +783,9 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter():
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -818,8 +825,9 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
         json=True,
     )
 
@@ -862,8 +870,9 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -903,8 +912,9 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
         json=True,
     )
 
@@ -946,8 +956,9 @@ def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter():
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -991,8 +1002,9 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats_paramete
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
         stats=True,
     )
 
@@ -1035,8 +1047,9 @@ def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_inte
             'exclude_patterns': None,
             'checkpoint_interval': 600,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -1078,8 +1091,9 @@ def test_create_archive_with_checkpoint_volume_calls_borg_with_checkpoint_volume
             'exclude_patterns': None,
             'checkpoint_volume': 1024,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -1121,8 +1135,9 @@ def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_param
             'exclude_patterns': None,
             'chunker_params': '1,2,3,4',
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -1164,8 +1179,9 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters(
             'exclude_patterns': None,
             'compression': 'rle',
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -1213,8 +1229,9 @@ def test_create_archive_with_upload_rate_limit_calls_borg_with_upload_ratelimit_
             'exclude_patterns': None,
             'upload_rate_limit': 100,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -1258,8 +1275,9 @@ def test_create_archive_with_working_directory_calls_borg_with_working_directory
             'working_directory': '/working/dir',
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -1301,8 +1319,9 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par
             'one_file_system': True,
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -1350,8 +1369,9 @@ def test_create_archive_with_numeric_ids_calls_borg_with_numeric_ids_parameter(
             'numeric_ids': True,
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -1403,8 +1423,9 @@ def test_create_archive_with_read_special_calls_borg_with_read_special_parameter
             'read_special': True,
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -1458,8 +1479,9 @@ def test_create_archive_with_basic_option_calls_borg_with_corresponding_paramete
             option_name: option_value,
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -1512,8 +1534,9 @@ def test_create_archive_with_atime_option_calls_borg_with_corresponding_paramete
             'atime': option_value,
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -1566,8 +1589,9 @@ def test_create_archive_with_flags_option_calls_borg_with_corresponding_paramete
             'flags': option_value,
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -1609,8 +1633,9 @@ def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters(
             'files_cache': 'ctime,size',
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -1651,8 +1676,9 @@ def test_create_archive_with_local_path_calls_borg_via_local_path():
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
         local_path='borg1',
     )
 
@@ -1694,8 +1720,9 @@ def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters(
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
         remote_path='borg1',
     )
 
@@ -1738,8 +1765,9 @@ def test_create_archive_with_umask_calls_borg_with_umask_parameters():
             'exclude_patterns': None,
             'umask': 740,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -1780,8 +1808,9 @@ def test_create_archive_with_log_json_calls_borg_with_log_json_parameters():
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=True, used_config_paths=[]),
+        global_arguments=flexmock(log_json=True),
     )
 
 
@@ -1823,8 +1852,9 @@ def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters():
             'exclude_patterns': None,
             'lock_wait': 5,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -1865,8 +1895,9 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_answer_ou
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
         stats=True,
     )
 
@@ -1908,8 +1939,9 @@ def test_create_archive_with_files_calls_borg_with_list_parameter_and_answer_out
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
         list_files=True,
     )
 
@@ -1952,8 +1984,9 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
         progress=True,
     )
 
@@ -1995,8 +2028,9 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter():
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
         progress=True,
     )
 
@@ -2057,8 +2091,9 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
         progress=True,
         stream_processes=processes,
     )
@@ -2121,8 +2156,9 @@ def test_create_archive_with_stream_processes_ignores_read_special_false_and_log
             'exclude_patterns': None,
             'read_special': False,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
         stream_processes=processes,
     )
 
@@ -2188,8 +2224,9 @@ def test_create_archive_with_stream_processes_adds_special_files_to_excludes():
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
         stream_processes=processes,
     )
 
@@ -2252,8 +2289,9 @@ def test_create_archive_with_stream_processes_and_read_special_does_not_add_spec
             'exclude_patterns': None,
             'read_special': True,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
         stream_processes=processes,
     )
 
@@ -2293,8 +2331,9 @@ def test_create_archive_with_json_calls_borg_with_json_parameter():
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
         json=True,
     )
 
@@ -2336,8 +2375,9 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter()
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
         json=True,
         stats=True,
     )
@@ -2383,8 +2423,9 @@ def test_create_archive_with_source_directories_glob_expands():
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -2426,8 +2467,9 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -2468,8 +2510,9 @@ def test_create_archive_with_glob_calls_borg_with_expanded_directories():
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -2511,8 +2554,9 @@ def test_create_archive_with_archive_name_format_calls_borg_with_archive_name():
             'exclude_patterns': None,
             'archive_name_format': 'ARCHIVE_NAME',
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -2555,8 +2599,9 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
             'exclude_patterns': None,
             'archive_name_format': 'Documents_{hostname}-{now}',  # noqa: FS003
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -2599,8 +2644,9 @@ def test_create_archive_with_repository_accepts_borg_placeholders():
             'exclude_patterns': None,
             'archive_name_format': 'Documents_{hostname}-{now}',  # noqa: FS003
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -2642,8 +2688,9 @@ def test_create_archive_with_extra_borg_options_calls_borg_with_extra_options():
             'exclude_patterns': None,
             'extra_borg_options': {'create': '--extra --options'},
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
     )
 
 
@@ -2702,8 +2749,9 @@ def test_create_archive_with_stream_processes_calls_borg_with_processes_and_read
             'repositories': ['repo'],
             'exclude_patterns': None,
         },
+        config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
-        global_arguments=flexmock(log_json=False, used_config_paths=[]),
+        global_arguments=flexmock(log_json=False),
         stream_processes=processes,
     )
 
@@ -2727,8 +2775,9 @@ def test_create_archive_with_non_existent_directory_and_source_directories_must_
                 'exclude_patterns': None,
                 'source_directories_must_exist': True,
             },
+            config_paths=['/tmp/test.yaml'],
             local_borg_version='1.2.3',
-            global_arguments=flexmock(log_json=False, used_config_paths=[]),
+            global_arguments=flexmock(log_json=False),
         )
 
 

+ 96 - 40
tests/unit/commands/test_borgmatic.py

@@ -38,7 +38,7 @@ def test_run_configuration_runs_actions_for_each_repository():
     config = {'repositories': [{'path': 'foo'}, {'path': 'bar'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1)}
 
-    results = list(module.run_configuration('test.yaml', config, arguments))
+    results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
 
     assert results == expected_results
 
@@ -51,7 +51,7 @@ def test_run_configuration_with_skip_actions_does_not_raise():
     config = {'repositories': [{'path': 'foo'}, {'path': 'bar'}], 'skip_actions': ['compact']}
     arguments = {'global': flexmock(monitoring_verbosity=1)}
 
-    list(module.run_configuration('test.yaml', config, arguments))
+    list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
 
 
 def test_run_configuration_with_invalid_borg_version_errors():
@@ -64,7 +64,7 @@ def test_run_configuration_with_invalid_borg_version_errors():
     config = {'repositories': ['foo']}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'prune': flexmock()}
 
-    list(module.run_configuration('test.yaml', config, arguments))
+    list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
 
 
 def test_run_configuration_logs_monitor_start_error():
@@ -80,7 +80,7 @@ def test_run_configuration_logs_monitor_start_error():
     config = {'repositories': ['foo']}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
 
-    results = list(module.run_configuration('test.yaml', config, arguments))
+    results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
 
     assert results == expected_results
 
@@ -96,7 +96,7 @@ def test_run_configuration_bails_for_monitor_start_soft_failure():
     config = {'repositories': ['foo']}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
 
-    results = list(module.run_configuration('test.yaml', config, arguments))
+    results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
 
     assert results == []
 
@@ -113,7 +113,7 @@ def test_run_configuration_logs_actions_error():
     config = {'repositories': [{'path': 'foo'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False)}
 
-    results = list(module.run_configuration('test.yaml', config, arguments))
+    results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
 
     assert results == expected_results
 
@@ -130,7 +130,7 @@ def test_run_configuration_bails_for_actions_soft_failure():
     config = {'repositories': [{'path': 'foo'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
 
-    results = list(module.run_configuration('test.yaml', config, arguments))
+    results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
 
     assert results == []
 
@@ -148,7 +148,7 @@ def test_run_configuration_logs_monitor_log_error():
     config = {'repositories': [{'path': 'foo'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
 
-    results = list(module.run_configuration('test.yaml', config, arguments))
+    results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
 
     assert results == expected_results
 
@@ -167,7 +167,7 @@ def test_run_configuration_bails_for_monitor_log_soft_failure():
     config = {'repositories': [{'path': 'foo'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
 
-    results = list(module.run_configuration('test.yaml', config, arguments))
+    results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
 
     assert results == []
 
@@ -185,7 +185,7 @@ def test_run_configuration_logs_monitor_finish_error():
     config = {'repositories': [{'path': 'foo'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
 
-    results = list(module.run_configuration('test.yaml', config, arguments))
+    results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
 
     assert results == expected_results
 
@@ -204,7 +204,7 @@ def test_run_configuration_bails_for_monitor_finish_soft_failure():
     config = {'repositories': [{'path': 'foo'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
 
-    results = list(module.run_configuration('test.yaml', config, arguments))
+    results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
 
     assert results == []
 
@@ -219,7 +219,7 @@ def test_run_configuration_does_not_call_monitoring_hooks_if_monitoring_hooks_ar
 
     config = {'repositories': [{'path': 'foo'}]}
     arguments = {'global': flexmock(monitoring_verbosity=-2, dry_run=False), 'create': flexmock()}
-    results = list(module.run_configuration('test.yaml', config, arguments))
+    results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
     assert results == []
 
 
@@ -236,7 +236,7 @@ def test_run_configuration_logs_on_error_hook_error():
     config = {'repositories': [{'path': 'foo'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
 
-    results = list(module.run_configuration('test.yaml', config, arguments))
+    results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
 
     assert results == expected_results
 
@@ -253,7 +253,7 @@ def test_run_configuration_bails_for_on_error_hook_soft_failure():
     config = {'repositories': [{'path': 'foo'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
 
-    results = list(module.run_configuration('test.yaml', config, arguments))
+    results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
 
     assert results == expected_results
 
@@ -268,7 +268,7 @@ def test_run_configuration_retries_soft_error():
     flexmock(module).should_receive('log_error_records').and_return([flexmock()]).once()
     config = {'repositories': [{'path': 'foo'}], 'retries': 1}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
-    results = list(module.run_configuration('test.yaml', config, arguments))
+    results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
     assert results == []
 
 
@@ -292,7 +292,7 @@ def test_run_configuration_retries_hard_error():
     ).and_return(error_logs)
     config = {'repositories': [{'path': 'foo'}], 'retries': 1}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
-    results = list(module.run_configuration('test.yaml', config, arguments))
+    results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
     assert results == error_logs
 
 
@@ -311,7 +311,7 @@ def test_run_configuration_repos_ordered():
     ).and_return(expected_results[1:]).ordered()
     config = {'repositories': [{'path': 'foo'}, {'path': 'bar'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
-    results = list(module.run_configuration('test.yaml', config, arguments))
+    results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
     assert results == expected_results
 
 
@@ -346,7 +346,7 @@ def test_run_configuration_retries_round_robin():
         'retries': 1,
     }
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
-    results = list(module.run_configuration('test.yaml', config, arguments))
+    results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
     assert results == foo_error_logs + bar_error_logs
 
 
@@ -379,7 +379,7 @@ def test_run_configuration_retries_one_passes():
         'retries': 1,
     }
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
-    results = list(module.run_configuration('test.yaml', config, arguments))
+    results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
     assert results == error_logs
 
 
@@ -423,7 +423,7 @@ def test_run_configuration_retry_wait():
         'retry_wait': 10,
     }
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
-    results = list(module.run_configuration('test.yaml', config, arguments))
+    results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
     assert results == error_logs
 
 
@@ -463,7 +463,7 @@ def test_run_configuration_retries_timeout_multiple_repos():
         'retry_wait': 10,
     }
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
-    results = list(module.run_configuration('test.yaml', config, arguments))
+    results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
     assert results == error_logs
 
 
@@ -478,6 +478,7 @@ def test_run_actions_runs_rcreate():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'rcreate': flexmock()},
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -495,6 +496,7 @@ def test_run_actions_adds_log_file_to_hook_context():
         config_filename=object,
         repository={'path': 'repo'},
         config={'repositories': []},
+        config_paths=[],
         hook_context={'repository': 'repo', 'repositories': '', 'log_file': 'foo'},
         local_borg_version=object,
         create_arguments=object,
@@ -509,6 +511,7 @@ def test_run_actions_adds_log_file_to_hook_context():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'create': flexmock()},
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -529,6 +532,7 @@ def test_run_actions_runs_transfer():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'transfer': flexmock()},
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -549,6 +553,7 @@ def test_run_actions_runs_create():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'create': flexmock()},
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -569,6 +574,7 @@ def test_run_actions_with_skip_actions_skips_create():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'create': flexmock()},
             config_filename=flexmock(),
             config={'repositories': [], 'skip_actions': ['create']},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -588,6 +594,7 @@ def test_run_actions_runs_prune():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'prune': flexmock()},
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -607,6 +614,7 @@ def test_run_actions_with_skip_actions_skips_prune():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'prune': flexmock()},
             config_filename=flexmock(),
             config={'repositories': [], 'skip_actions': ['prune']},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -626,6 +634,7 @@ def test_run_actions_runs_compact():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'compact': flexmock()},
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -645,6 +654,7 @@ def test_run_actions_with_skip_actions_skips_compact():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'compact': flexmock()},
             config_filename=flexmock(),
             config={'repositories': [], 'skip_actions': ['compact']},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -665,6 +675,7 @@ def test_run_actions_runs_check_when_repository_enabled_for_checks():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'check': flexmock()},
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -685,6 +696,7 @@ def test_run_actions_skips_check_when_repository_not_enabled_for_checks():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'check': flexmock()},
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -705,6 +717,7 @@ def test_run_actions_with_skip_actions_skips_check():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'check': flexmock()},
             config_filename=flexmock(),
             config={'repositories': [], 'skip_actions': ['check']},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -724,6 +737,7 @@ def test_run_actions_runs_extract():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'extract': flexmock()},
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -743,6 +757,7 @@ def test_run_actions_runs_export_tar():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'export-tar': flexmock()},
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -762,6 +777,7 @@ def test_run_actions_runs_mount():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'mount': flexmock()},
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -781,6 +797,7 @@ def test_run_actions_runs_restore():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'restore': flexmock()},
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -801,6 +818,7 @@ def test_run_actions_runs_rlist():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'rlist': flexmock()},
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -822,6 +840,7 @@ def test_run_actions_runs_list():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'list': flexmock()},
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -843,6 +862,7 @@ def test_run_actions_runs_rinfo():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'rinfo': flexmock()},
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -864,6 +884,7 @@ def test_run_actions_runs_info():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'info': flexmock()},
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -884,6 +905,7 @@ def test_run_actions_runs_break_lock():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'break-lock': flexmock()},
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -903,6 +925,7 @@ def test_run_actions_runs_export_key():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'export': flexmock()},
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -922,6 +945,7 @@ def test_run_actions_runs_borg():
             arguments={'global': flexmock(dry_run=False, log_file='foo'), 'borg': flexmock()},
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -946,6 +970,7 @@ def test_run_actions_runs_multiple_actions_in_argument_order():
             },
             config_filename=flexmock(),
             config={'repositories': []},
+            config_paths=[],
             local_path=flexmock(),
             remote_path=flexmock(),
             local_borg_version=flexmock(),
@@ -960,30 +985,33 @@ def test_load_configurations_collects_parsed_configurations_and_logs():
     test_expected_logs = [flexmock(), flexmock()]
     other_expected_logs = [flexmock(), flexmock()]
     flexmock(module.validate).should_receive('parse_configuration').and_return(
-        configuration, test_expected_logs
-    ).and_return(other_configuration, other_expected_logs)
+        configuration, ['/tmp/test.yaml'], test_expected_logs
+    ).and_return(other_configuration, ['/tmp/other.yaml'], other_expected_logs)
 
-    configs, logs = tuple(module.load_configurations(('test.yaml', 'other.yaml')))
+    configs, config_paths, logs = tuple(module.load_configurations(('test.yaml', 'other.yaml')))
 
     assert configs == {'test.yaml': configuration, 'other.yaml': other_configuration}
+    assert config_paths == ['/tmp/other.yaml', '/tmp/test.yaml']
     assert set(logs) >= set(test_expected_logs + other_expected_logs)
 
 
 def test_load_configurations_logs_warning_for_permission_error():
     flexmock(module.validate).should_receive('parse_configuration').and_raise(PermissionError)
 
-    configs, logs = tuple(module.load_configurations(('test.yaml',)))
+    configs, config_paths, logs = tuple(module.load_configurations(('test.yaml',)))
 
     assert configs == {}
+    assert config_paths == []
     assert max(log.levelno for log in logs) == logging.WARNING
 
 
 def test_load_configurations_logs_critical_for_parse_error():
     flexmock(module.validate).should_receive('parse_configuration').and_raise(ValueError)
 
-    configs, logs = tuple(module.load_configurations(('test.yaml',)))
+    configs, config_paths, logs = tuple(module.load_configurations(('test.yaml',)))
 
     assert configs == {}
+    assert config_paths == []
     assert max(log.levelno for log in logs) == logging.CRITICAL
 
 
@@ -1214,7 +1242,9 @@ def test_collect_configuration_run_summary_logs_info_for_success():
     arguments = {}
 
     logs = tuple(
-        module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
+        module.collect_configuration_run_summary_logs(
+            {'test.yaml': {}}, config_paths=['/tmp/test.yaml'], arguments=arguments
+        )
     )
 
     assert {log.levelno for log in logs} == {logging.INFO}
@@ -1226,7 +1256,9 @@ def test_collect_configuration_run_summary_executes_hooks_for_create():
     arguments = {'create': flexmock(), 'global': flexmock(monitoring_verbosity=1, dry_run=False)}
 
     logs = tuple(
-        module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
+        module.collect_configuration_run_summary_logs(
+            {'test.yaml': {}}, config_paths=['/tmp/test.yaml'], arguments=arguments
+        )
     )
 
     assert {log.levelno for log in logs} == {logging.INFO}
@@ -1239,7 +1271,9 @@ def test_collect_configuration_run_summary_logs_info_for_success_with_extract():
     arguments = {'extract': flexmock(repository='repo')}
 
     logs = tuple(
-        module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
+        module.collect_configuration_run_summary_logs(
+            {'test.yaml': {}}, config_paths=['/tmp/test.yaml'], arguments=arguments
+        )
     )
 
     assert {log.levelno for log in logs} == {logging.INFO}
@@ -1254,7 +1288,9 @@ def test_collect_configuration_run_summary_logs_extract_with_repository_error():
     arguments = {'extract': flexmock(repository='repo')}
 
     logs = tuple(
-        module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
+        module.collect_configuration_run_summary_logs(
+            {'test.yaml': {}}, config_paths=['/tmp/test.yaml'], arguments=arguments
+        )
     )
 
     assert logs == expected_logs
@@ -1267,7 +1303,9 @@ def test_collect_configuration_run_summary_logs_info_for_success_with_mount():
     arguments = {'mount': flexmock(repository='repo')}
 
     logs = tuple(
-        module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
+        module.collect_configuration_run_summary_logs(
+            {'test.yaml': {}}, config_paths=['/tmp/test.yaml'], arguments=arguments
+        )
     )
 
     assert {log.levelno for log in logs} == {logging.INFO}
@@ -1282,7 +1320,9 @@ def test_collect_configuration_run_summary_logs_mount_with_repository_error():
     arguments = {'mount': flexmock(repository='repo')}
 
     logs = tuple(
-        module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
+        module.collect_configuration_run_summary_logs(
+            {'test.yaml': {}}, config_paths=['/tmp/test.yaml'], arguments=arguments
+        )
     )
 
     assert logs == expected_logs
@@ -1293,7 +1333,9 @@ def test_collect_configuration_run_summary_logs_missing_configs_error():
     expected_logs = (flexmock(),)
     flexmock(module).should_receive('log_error_records').and_return(expected_logs)
 
-    logs = tuple(module.collect_configuration_run_summary_logs({}, arguments=arguments))
+    logs = tuple(
+        module.collect_configuration_run_summary_logs({}, config_paths=[], arguments=arguments)
+    )
 
     assert logs == expected_logs
 
@@ -1305,7 +1347,9 @@ def test_collect_configuration_run_summary_logs_pre_hook_error():
     arguments = {'create': flexmock(), 'global': flexmock(monitoring_verbosity=1, dry_run=False)}
 
     logs = tuple(
-        module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
+        module.collect_configuration_run_summary_logs(
+            {'test.yaml': {}}, config_paths=['/tmp/test.yaml'], arguments=arguments
+        )
     )
 
     assert logs == expected_logs
@@ -1320,7 +1364,9 @@ def test_collect_configuration_run_summary_logs_post_hook_error():
     arguments = {'create': flexmock(), 'global': flexmock(monitoring_verbosity=1, dry_run=False)}
 
     logs = tuple(
-        module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
+        module.collect_configuration_run_summary_logs(
+            {'test.yaml': {}}, config_paths=['/tmp/test.yaml'], arguments=arguments
+        )
     )
 
     assert expected_logs[0] in logs
@@ -1335,7 +1381,9 @@ def test_collect_configuration_run_summary_logs_for_list_with_archive_and_reposi
     arguments = {'list': flexmock(repository='repo', archive='test')}
 
     logs = tuple(
-        module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
+        module.collect_configuration_run_summary_logs(
+            {'test.yaml': {}}, config_paths=['/tmp/test.yaml'], arguments=arguments
+        )
     )
 
     assert logs == expected_logs
@@ -1347,7 +1395,9 @@ def test_collect_configuration_run_summary_logs_info_for_success_with_list():
     arguments = {'list': flexmock(repository='repo', archive=None)}
 
     logs = tuple(
-        module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
+        module.collect_configuration_run_summary_logs(
+            {'test.yaml': {}}, config_paths=['/tmp/test.yaml'], arguments=arguments
+        )
     )
 
     assert {log.levelno for log in logs} == {logging.INFO}
@@ -1362,7 +1412,9 @@ def test_collect_configuration_run_summary_logs_run_configuration_error():
     arguments = {}
 
     logs = tuple(
-        module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
+        module.collect_configuration_run_summary_logs(
+            {'test.yaml': {}}, config_paths=['/tmp/test.yaml'], arguments=arguments
+        )
     )
 
     assert {log.levelno for log in logs} == {logging.CRITICAL}
@@ -1378,7 +1430,9 @@ def test_collect_configuration_run_summary_logs_run_umount_error():
     arguments = {'umount': flexmock(mount_point='/mnt')}
 
     logs = tuple(
-        module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
+        module.collect_configuration_run_summary_logs(
+            {'test.yaml': {}}, config_paths=['/tmp/test.yaml'], arguments=arguments
+        )
     )
 
     assert {log.levelno for log in logs} == {logging.INFO, logging.CRITICAL}
@@ -1396,6 +1450,8 @@ def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
 
     tuple(
         module.collect_configuration_run_summary_logs(
-            {'test.yaml': {}, 'test2.yaml': {}}, arguments=arguments
+            {'test.yaml': {}, 'test2.yaml': {}},
+            config_paths=['/tmp/test.yaml', '/tmp/test2.yaml'],
+            arguments=arguments,
         )
     )

+ 19 - 11
tests/unit/config/test_load.py

@@ -6,27 +6,35 @@ from borgmatic.config import load as module
 
 def test_probe_and_include_file_with_absolute_path_skips_probing():
     config = flexmock()
-    flexmock(module).should_receive('load_configuration').with_args('/etc/include.yaml').and_return(
-        config
-    ).once()
+    config_paths = set()
+    flexmock(module).should_receive('load_configuration').with_args(
+        '/etc/include.yaml', config_paths
+    ).and_return(config).once()
 
-    assert module.probe_and_include_file('/etc/include.yaml', ['/etc', '/var']) == config
+    assert (
+        module.probe_and_include_file('/etc/include.yaml', ['/etc', '/var'], config_paths) == config
+    )
 
 
 def test_probe_and_include_file_with_relative_path_probes_include_directories():
-    config = flexmock()
+    config = {'foo': 'bar'}
+    config_paths = set()
     flexmock(module.os.path).should_receive('exists').with_args('/etc/include.yaml').and_return(
         False
     )
     flexmock(module.os.path).should_receive('exists').with_args('/var/include.yaml').and_return(
         True
     )
-    flexmock(module).should_receive('load_configuration').with_args('/etc/include.yaml').never()
-    flexmock(module).should_receive('load_configuration').with_args('/var/include.yaml').and_return(
-        config
-    ).once()
+    flexmock(module).should_receive('load_configuration').with_args(
+        '/etc/include.yaml', config_paths
+    ).never()
+    flexmock(module).should_receive('load_configuration').with_args(
+        '/var/include.yaml', config_paths
+    ).and_return(config).once()
 
-    assert module.probe_and_include_file('include.yaml', ['/etc', '/var']) == config
+    assert module.probe_and_include_file('include.yaml', ['/etc', '/var'], config_paths) == {
+        'foo': 'bar',
+    }
 
 
 def test_probe_and_include_file_with_relative_path_and_missing_files_raises():
@@ -34,4 +42,4 @@ def test_probe_and_include_file_with_relative_path_and_missing_files_raises():
     flexmock(module).should_receive('load_configuration').never()
 
     with pytest.raises(FileNotFoundError):
-        module.probe_and_include_file('include.yaml', ['/etc', '/var'])
+        module.probe_and_include_file('include.yaml', ['/etc', '/var'], config_paths=set())