Browse Source

Fix existing tests (#1019).

Dan Helfman 4 months ago
parent
commit
6a96a78cf1

+ 399 - 430
borgmatic/commands/borgmatic.py

@@ -96,135 +96,39 @@ def run_configuration(config_filename, config, config_paths, arguments):
             f"Skipping {'/'.join(skip_actions)} action{'s' if len(skip_actions) > 1 else ''} due to configured skip_actions"
             f"Skipping {'/'.join(skip_actions)} action{'s' if len(skip_actions) > 1 else ''} due to configured skip_actions"
         )
         )
 
 
-    command.execute_hooks(
-        command.filter_hooks(
-            config.get('commands'), before='configuration', action_names=arguments.keys()
-        ),
-        config.get('umask'),
-        global_arguments.dry_run,
-        configuration_filename=config_filename,
-    )
-
-    try:
-        local_borg_version = borg_version.local_borg_version(config, local_path)
-        logger.debug(f'Borg {local_borg_version}')
-    except (OSError, CalledProcessError, ValueError) as error:
-        yield from log_error_records(f'{config_filename}: Error getting local Borg version', error)
-        return
-
-    try:
-        if monitoring_hooks_are_activated:
-            dispatch.call_hooks(
-                'initialize_monitor',
-                config,
-                dispatch.Hook_type.MONITORING,
-                config_filename,
-                monitoring_log_level,
-                global_arguments.dry_run,
-            )
-
-            dispatch.call_hooks(
-                'ping_monitor',
-                config,
-                dispatch.Hook_type.MONITORING,
-                config_filename,
-                monitor.State.START,
-                monitoring_log_level,
-                global_arguments.dry_run,
+    with borgmatic.hooks.command.Before_after_hooks(
+        command_hooks=config.get('commands'),
+        before_after='configuration',
+        umask=config.get('umask'),
+        dry_run=global_arguments.dry_run,
+        action_names=arguments.keys(),
+    ):
+        try:
+            local_borg_version = borg_version.local_borg_version(config, local_path)
+            logger.debug(f'Borg {local_borg_version}')
+        except (OSError, CalledProcessError, ValueError) as error:
+            yield from log_error_records(
+                f'{config_filename}: Error getting local Borg version', error
             )
             )
-    except (OSError, CalledProcessError) as error:
-        if command.considered_soft_failure(error):
             return
             return
 
 
-        encountered_error = error
-        yield from log_error_records(f'{config_filename}: Error pinging monitor', error)
-
-    if not encountered_error:
-        repo_queue = Queue()
-        for repo in config['repositories']:
-            repo_queue.put(
-                (repo, 0),
-            )
-
-        while not repo_queue.empty():
-            repository, retry_num = repo_queue.get()
-
-            with Log_prefix(repository.get('label', repository['path'])):
-                logger.debug('Running actions for repository')
-                timeout = retry_num * retry_wait
-                if timeout:
-                    logger.warning(f'Sleeping {timeout}s before next retry')
-                    time.sleep(timeout)
-                try:
-                    yield from run_actions(
-                        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,
-                        repository=repository,
-                    )
-                except (OSError, CalledProcessError, ValueError) as error:
-                    if retry_num < retries:
-                        repo_queue.put(
-                            (repository, retry_num + 1),
-                        )
-                        tuple(  # Consume the generator so as to trigger logging.
-                            log_error_records(
-                                'Error running actions for repository',
-                                error,
-                                levelno=logging.WARNING,
-                                log_command_error_output=True,
-                            )
-                        )
-                        logger.warning(f'Retrying... attempt {retry_num + 1}/{retries}')
-                        continue
-
-                    if command.considered_soft_failure(error):
-                        continue
-
-                    yield from log_error_records(
-                        'Error running actions for repository',
-                        error,
-                    )
-                    encountered_error = error
-                    error_repository = repository['path']
-
-    try:
-        if monitoring_hooks_are_activated:
-            # Send logs irrespective of error.
-            dispatch.call_hooks(
-                'ping_monitor',
-                config,
-                dispatch.Hook_type.MONITORING,
-                config_filename,
-                monitor.State.LOG,
-                monitoring_log_level,
-                global_arguments.dry_run,
-            )
-    except (OSError, CalledProcessError) as error:
-        if not command.considered_soft_failure(error):
-            encountered_error = error
-            yield from log_error_records('Error pinging monitor', error)
-
-    if not encountered_error:
         try:
         try:
             if monitoring_hooks_are_activated:
             if monitoring_hooks_are_activated:
                 dispatch.call_hooks(
                 dispatch.call_hooks(
-                    'ping_monitor',
+                    'initialize_monitor',
                     config,
                     config,
                     dispatch.Hook_type.MONITORING,
                     dispatch.Hook_type.MONITORING,
                     config_filename,
                     config_filename,
-                    monitor.State.FINISH,
                     monitoring_log_level,
                     monitoring_log_level,
                     global_arguments.dry_run,
                     global_arguments.dry_run,
                 )
                 )
+
                 dispatch.call_hooks(
                 dispatch.call_hooks(
-                    'destroy_monitor',
+                    'ping_monitor',
                     config,
                     config,
                     dispatch.Hook_type.MONITORING,
                     dispatch.Hook_type.MONITORING,
+                    config_filename,
+                    monitor.State.START,
                     monitoring_log_level,
                     monitoring_log_level,
                     global_arguments.dry_run,
                     global_arguments.dry_run,
                 )
                 )
@@ -235,51 +139,138 @@ def run_configuration(config_filename, config, config_paths, arguments):
             encountered_error = error
             encountered_error = error
             yield from log_error_records(f'{config_filename}: Error pinging monitor', error)
             yield from log_error_records(f'{config_filename}: Error pinging monitor', error)
 
 
-    if encountered_error:
-        command.execute_hooks(
-            command.filter_hooks(
-                config.get('commands'), after='error', action_names=arguments.keys()
-            ),
-            config.get('umask'),
-            global_arguments.dry_run,
-            configuration_filename=config_filename,
-            repository=error_repository,
-            error=encountered_error,
-            output=getattr(encountered_error, 'output', ''),
-        )
+        if not encountered_error:
+            repo_queue = Queue()
+            for repo in config['repositories']:
+                repo_queue.put(
+                    (repo, 0),
+                )
 
 
-    command.execute_hooks(
-        command.filter_hooks(
-            config.get('commands'), after='configuration', action_names=arguments.keys()
-        ),
-        config.get('umask'),
-        global_arguments.dry_run,
-        configuration_filename=config_filename,
-    )
+            while not repo_queue.empty():
+                repository, retry_num = repo_queue.get()
+
+                with Log_prefix(repository.get('label', repository['path'])):
+                    logger.debug('Running actions for repository')
+                    timeout = retry_num * retry_wait
+                    if timeout:
+                        logger.warning(f'Sleeping {timeout}s before next retry')
+                        time.sleep(timeout)
+                    try:
+                        yield from run_actions(
+                            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,
+                            repository=repository,
+                        )
+                    except (OSError, CalledProcessError, ValueError) as error:
+                        if retry_num < retries:
+                            repo_queue.put(
+                                (repository, retry_num + 1),
+                            )
+                            tuple(  # Consume the generator so as to trigger logging.
+                                log_error_records(
+                                    'Error running actions for repository',
+                                    error,
+                                    levelno=logging.WARNING,
+                                    log_command_error_output=True,
+                                )
+                            )
+                            logger.warning(f'Retrying... attempt {retry_num + 1}/{retries}')
+                            continue
+
+                        if command.considered_soft_failure(error):
+                            continue
+
+                        yield from log_error_records(
+                            'Error running actions for repository',
+                            error,
+                        )
+                        encountered_error = error
+                        error_repository = repository['path']
 
 
-    if encountered_error and using_primary_action:
         try:
         try:
-            dispatch.call_hooks(
-                'ping_monitor',
-                config,
-                dispatch.Hook_type.MONITORING,
-                config_filename,
-                monitor.State.FAIL,
-                monitoring_log_level,
-                global_arguments.dry_run,
-            )
-            dispatch.call_hooks(
-                'destroy_monitor',
-                config,
-                dispatch.Hook_type.MONITORING,
-                monitoring_log_level,
-                global_arguments.dry_run,
-            )
+            if monitoring_hooks_are_activated:
+                # Send logs irrespective of error.
+                dispatch.call_hooks(
+                    'ping_monitor',
+                    config,
+                    dispatch.Hook_type.MONITORING,
+                    config_filename,
+                    monitor.State.LOG,
+                    monitoring_log_level,
+                    global_arguments.dry_run,
+                )
         except (OSError, CalledProcessError) as error:
         except (OSError, CalledProcessError) as error:
-            if command.considered_soft_failure(error):
-                return
+            if not command.considered_soft_failure(error):
+                encountered_error = error
+                yield from log_error_records('Error pinging monitor', error)
 
 
-            yield from log_error_records(f'{config_filename}: Error running on-error hook', error)
+        if not encountered_error:
+            try:
+                if monitoring_hooks_are_activated:
+                    dispatch.call_hooks(
+                        'ping_monitor',
+                        config,
+                        dispatch.Hook_type.MONITORING,
+                        config_filename,
+                        monitor.State.FINISH,
+                        monitoring_log_level,
+                        global_arguments.dry_run,
+                    )
+                    dispatch.call_hooks(
+                        'destroy_monitor',
+                        config,
+                        dispatch.Hook_type.MONITORING,
+                        monitoring_log_level,
+                        global_arguments.dry_run,
+                    )
+            except (OSError, CalledProcessError) as error:
+                if command.considered_soft_failure(error):
+                    return
+
+                encountered_error = error
+                yield from log_error_records(f'{config_filename}: Error pinging monitor', error)
+
+        if encountered_error:
+            try:
+                command.execute_hooks(
+                    command.filter_hooks(
+                        config.get('commands'), after='error', action_names=arguments.keys()
+                    ),
+                    config.get('umask'),
+                    global_arguments.dry_run,
+                    configuration_filename=config_filename,
+                    repository=error_repository,
+                    error=encountered_error,
+                    output=getattr(encountered_error, 'output', ''),
+                )
+                dispatch.call_hooks(
+                    'ping_monitor',
+                    config,
+                    dispatch.Hook_type.MONITORING,
+                    config_filename,
+                    monitor.State.FAIL,
+                    monitoring_log_level,
+                    global_arguments.dry_run,
+                )
+                dispatch.call_hooks(
+                    'destroy_monitor',
+                    config,
+                    dispatch.Hook_type.MONITORING,
+                    monitoring_log_level,
+                    global_arguments.dry_run,
+                )
+            except (OSError, CalledProcessError) as error:
+                if command.considered_soft_failure(error):
+                    return
+
+                yield from log_error_records(
+                    f'{config_filename}: Error running after error hook', error
+                )
 
 
 
 
 def run_actions(
 def run_actions(
@@ -319,256 +310,236 @@ def run_actions(
     }
     }
     skip_actions = set(get_skip_actions(config, arguments))
     skip_actions = set(get_skip_actions(config, arguments))
 
 
-    command.execute_hooks(
-        command.filter_hooks(
-            config.get('commands'), before='repository', action_names=arguments.keys()
-        ),
-        config.get('umask'),
-        global_arguments.dry_run,
-        **hook_context,
-    )
-
-    for action_name, action_arguments in arguments.items():
-        if action_name == 'global':
-            continue
-
-        command.execute_hooks(
-            command.filter_hooks(
-                config.get('commands'), before='action', action_names=arguments.keys()
-            ),
-            config.get('umask'),
-            global_arguments.dry_run,
-            **hook_context,
-        )
-
-        if action_name == 'repo-create' and action_name not in skip_actions:
-            borgmatic.actions.repo_create.run_repo_create(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'transfer' and action_name not in skip_actions:
-            borgmatic.actions.transfer.run_transfer(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'create' and action_name not in skip_actions:
-            yield from borgmatic.actions.create.run_create(
-                config_filename,
-                repository,
-                config,
-                config_paths,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                dry_run_label,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'prune' and action_name not in skip_actions:
-            borgmatic.actions.prune.run_prune(
-                config_filename,
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                dry_run_label,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'compact' and action_name not in skip_actions:
-            borgmatic.actions.compact.run_compact(
-                config_filename,
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                dry_run_label,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'check' and action_name not in skip_actions:
-            if checks.repository_enabled_for_checks(repository, config):
-                borgmatic.actions.check.run_check(
-                    config_filename,
-                    repository,
-                    config,
-                    local_borg_version,
-                    action_arguments,
-                    global_arguments,
-                    local_path,
-                    remote_path,
-                )
-        elif action_name == 'extract' and action_name not in skip_actions:
-            borgmatic.actions.extract.run_extract(
-                config_filename,
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'export-tar' and action_name not in skip_actions:
-            borgmatic.actions.export_tar.run_export_tar(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'mount' and action_name not in skip_actions:
-            borgmatic.actions.mount.run_mount(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'restore' and action_name not in skip_actions:
-            borgmatic.actions.restore.run_restore(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'repo-list' and action_name not in skip_actions:
-            yield from borgmatic.actions.repo_list.run_repo_list(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'list' and action_name not in skip_actions:
-            yield from borgmatic.actions.list.run_list(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'repo-info' and action_name not in skip_actions:
-            yield from borgmatic.actions.repo_info.run_repo_info(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'info' and action_name not in skip_actions:
-            yield from borgmatic.actions.info.run_info(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'break-lock' and action_name not in skip_actions:
-            borgmatic.actions.break_lock.run_break_lock(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'export' and action_name not in skip_actions:
-            borgmatic.actions.export_key.run_export_key(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'change-passphrase' and action_name not in skip_actions:
-            borgmatic.actions.change_passphrase.run_change_passphrase(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'delete' and action_name not in skip_actions:
-            borgmatic.actions.delete.run_delete(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'repo-delete' and action_name not in skip_actions:
-            borgmatic.actions.repo_delete.run_repo_delete(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-        elif action_name == 'borg' and action_name not in skip_actions:
-            borgmatic.actions.borg.run_borg(
-                repository,
-                config,
-                local_borg_version,
-                action_arguments,
-                global_arguments,
-                local_path,
-                remote_path,
-            )
-
-        command.execute_hooks(
-            command.filter_hooks(
-                config.get('commands'), after='action', action_names=arguments.keys()
-            ),
-            config.get('umask'),
-            global_arguments.dry_run,
-            **hook_context,
-        )
-
-    command.execute_hooks(
-        command.filter_hooks(
-            config.get('commands'), after='repository', action_names=arguments.keys()
-        ),
-        config.get('umask'),
-        global_arguments.dry_run,
+    with borgmatic.hooks.command.Before_after_hooks(
+        command_hooks=config.get('commands'),
+        before_after='repository',
+        umask=config.get('umask'),
+        dry_run=global_arguments.dry_run,
+        action_names=arguments.keys(),
         **hook_context,
         **hook_context,
-    )
+    ):
+        for action_name, action_arguments in arguments.items():
+            if action_name == 'global':
+                continue
+
+            with borgmatic.hooks.command.Before_after_hooks(
+                command_hooks=config.get('commands'),
+                before_after='action',
+                umask=config.get('umask'),
+                dry_run=global_arguments.dry_run,
+                action_names=arguments.keys(),
+                **hook_context,
+            ):
+                if action_name == 'repo-create' and action_name not in skip_actions:
+                    borgmatic.actions.repo_create.run_repo_create(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'transfer' and action_name not in skip_actions:
+                    borgmatic.actions.transfer.run_transfer(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'create' and action_name not in skip_actions:
+                    yield from borgmatic.actions.create.run_create(
+                        config_filename,
+                        repository,
+                        config,
+                        config_paths,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        dry_run_label,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'prune' and action_name not in skip_actions:
+                    borgmatic.actions.prune.run_prune(
+                        config_filename,
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        dry_run_label,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'compact' and action_name not in skip_actions:
+                    borgmatic.actions.compact.run_compact(
+                        config_filename,
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        dry_run_label,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'check' and action_name not in skip_actions:
+                    if checks.repository_enabled_for_checks(repository, config):
+                        borgmatic.actions.check.run_check(
+                            config_filename,
+                            repository,
+                            config,
+                            local_borg_version,
+                            action_arguments,
+                            global_arguments,
+                            local_path,
+                            remote_path,
+                        )
+                elif action_name == 'extract' and action_name not in skip_actions:
+                    borgmatic.actions.extract.run_extract(
+                        config_filename,
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'export-tar' and action_name not in skip_actions:
+                    borgmatic.actions.export_tar.run_export_tar(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'mount' and action_name not in skip_actions:
+                    borgmatic.actions.mount.run_mount(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'restore' and action_name not in skip_actions:
+                    borgmatic.actions.restore.run_restore(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'repo-list' and action_name not in skip_actions:
+                    yield from borgmatic.actions.repo_list.run_repo_list(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'list' and action_name not in skip_actions:
+                    yield from borgmatic.actions.list.run_list(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'repo-info' and action_name not in skip_actions:
+                    yield from borgmatic.actions.repo_info.run_repo_info(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'info' and action_name not in skip_actions:
+                    yield from borgmatic.actions.info.run_info(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'break-lock' and action_name not in skip_actions:
+                    borgmatic.actions.break_lock.run_break_lock(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'export' and action_name not in skip_actions:
+                    borgmatic.actions.export_key.run_export_key(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'change-passphrase' and action_name not in skip_actions:
+                    borgmatic.actions.change_passphrase.run_change_passphrase(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'delete' and action_name not in skip_actions:
+                    borgmatic.actions.delete.run_delete(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'repo-delete' and action_name not in skip_actions:
+                    borgmatic.actions.repo_delete.run_repo_delete(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
+                elif action_name == 'borg' and action_name not in skip_actions:
+                    borgmatic.actions.borg.run_borg(
+                        repository,
+                        config,
+                        local_borg_version,
+                        action_arguments,
+                        global_arguments,
+                        local_path,
+                        remote_path,
+                    )
 
 
 
 
 def load_configurations(config_filenames, overrides=None, resolve_env=True):
 def load_configurations(config_filenames, overrides=None, resolve_env=True):
@@ -848,20 +819,19 @@ def collect_configuration_run_summary_logs(configs, config_paths, arguments):
         )
         )
         return
         return
 
 
-    if 'create' in arguments:
-        try:
-            for config_filename, config in configs.items():
-                command.execute_hooks(
-                    command.filter_hooks(
-                        config.get('commands'), before='everything', action_names=arguments.keys()
-                    ),
-                    config.get('umask'),
-                    arguments['global'].dry_run,
-                    configuration_filename=config_filename,
-                )
-        except (CalledProcessError, ValueError, OSError) as error:
-            yield from log_error_records('Error running pre-everything hook', error)
-            return
+    try:
+        for config_filename, config in configs.items():
+            command.execute_hooks(
+                command.filter_hooks(
+                    config.get('commands'), before='everything', action_names=arguments.keys()
+                ),
+                config.get('umask'),
+                arguments['global'].dry_run,
+                configuration_filename=config_filename,
+            )
+    except (CalledProcessError, ValueError, OSError) as error:
+        yield from log_error_records('Error running pre-everything hook', error)
+        return
 
 
     # Execute the actions corresponding to each configuration file.
     # Execute the actions corresponding to each configuration file.
     json_results = []
     json_results = []
@@ -901,19 +871,18 @@ def collect_configuration_run_summary_logs(configs, config_paths, arguments):
     if json_results:
     if json_results:
         sys.stdout.write(json.dumps(json_results))
         sys.stdout.write(json.dumps(json_results))
 
 
-    if 'create' in arguments:
-        try:
-            for config_filename, config in configs.items():
-                command.execute_hooks(
-                    command.filter_hooks(
-                        config.get('commands'), after='everything', action_names=arguments.keys()
-                    ),
-                    config.get('umask'),
-                    arguments['global'].dry_run,
-                    configuration_filename=config_filename,
-                )
-        except (CalledProcessError, ValueError, OSError) as error:
-            yield from log_error_records('Error running post-everything hook', error)
+    try:
+        for config_filename, config in configs.items():
+            command.execute_hooks(
+                command.filter_hooks(
+                    config.get('commands'), after='everything', action_names=arguments.keys()
+                ),
+                config.get('umask'),
+                arguments['global'].dry_run,
+                configuration_filename=config_filename,
+            )
+    except (CalledProcessError, ValueError, OSError) as error:
+        yield from log_error_records('Error running post-everything hook', error)
 
 
 
 
 def exit_with_help_link():  # pragma: no cover
 def exit_with_help_link():  # pragma: no cover

+ 11 - 4
borgmatic/config/generate.py

@@ -43,10 +43,13 @@ def get_properties(schema):
     return schema['properties']
     return schema['properties']
 
 
 
 
-def schema_to_sample_configuration(schema, source_config, level=0, parent_is_sequence=False):
+def schema_to_sample_configuration(schema, source_config=None, level=0, parent_is_sequence=False):
     '''
     '''
     Given a loaded configuration schema and a source configuration, generate and return sample
     Given a loaded configuration schema and a source configuration, generate and return sample
     config for the schema. Include comments for each option based on the schema "description".
     config for the schema. Include comments for each option based on the schema "description".
+
+    If a source config is given, walk it alongside the given schema so that both can be taken into
+    account when commenting out particular options in add_comments_to_configuration_object().
     '''
     '''
     schema_type = schema.get('type')
     schema_type = schema.get('type')
     example = schema.get('example')
     example = schema.get('example')
@@ -72,7 +75,7 @@ def schema_to_sample_configuration(schema, source_config, level=0, parent_is_seq
                 (
                 (
                     field_name,
                     field_name,
                     schema_to_sample_configuration(
                     schema_to_sample_configuration(
-                        sub_schema, source_config.get(field_name, {}), level + 1
+                        sub_schema, (source_config or {}).get(field_name, {}), level + 1
                     ),
                     ),
                 )
                 )
                 for field_name, sub_schema in get_properties(schema).items()
                 for field_name, sub_schema in get_properties(schema).items()
@@ -204,7 +207,9 @@ DEFAULT_KEYS = {'source_directories', 'repositories', 'keep_daily'}
 COMMENTED_OUT_SENTINEL = 'COMMENT_OUT'
 COMMENTED_OUT_SENTINEL = 'COMMENT_OUT'
 
 
 
 
-def add_comments_to_configuration_object(config, schema, source_config, indent=0, skip_first=False):
+def add_comments_to_configuration_object(
+    config, schema, source_config=None, indent=0, skip_first=False
+):
     '''
     '''
     Using descriptions from a schema as a source, add those descriptions as comments to the given
     Using descriptions from a schema as a source, add those descriptions as comments to the given
     configuration dict, putting them before each field. Indent the comment the given number of
     configuration dict, putting them before each field. Indent the comment the given number of
@@ -224,7 +229,9 @@ def add_comments_to_configuration_object(config, schema, source_config, indent=0
         # If this isn't a default key, add an indicator to the comment flagging it to be commented
         # If this isn't a default key, add an indicator to the comment flagging it to be commented
         # out from the sample configuration. This sentinel is consumed by downstream processing that
         # out from the sample configuration. This sentinel is consumed by downstream processing that
         # does the actual commenting out.
         # does the actual commenting out.
-        if field_name not in DEFAULT_KEYS and field_name not in source_config:
+        if field_name not in DEFAULT_KEYS and (
+            source_config is None or field_name not in source_config
+        ):
             description = (
             description = (
                 '\n'.join((description, COMMENTED_OUT_SENTINEL))
                 '\n'.join((description, COMMENTED_OUT_SENTINEL))
                 if description
                 if description

+ 1 - 0
borgmatic/config/normalize.py

@@ -134,6 +134,7 @@ def normalize_commands(config_filename, config):
             config.setdefault('commands', []).append(
             config.setdefault('commands', []).append(
                 {
                 {
                     preposition: 'everything',
                     preposition: 'everything',
+                    'when': ['create'],
                     'run': commands,
                     'run': commands,
                 }
                 }
             )
             )

+ 5 - 3
borgmatic/config/schema.yaml

@@ -1015,7 +1015,7 @@ properties:
                               List of actions for which the commands will be
                               List of actions for which the commands will be
                               run. Defaults to running for all actions. Ignored
                               run. Defaults to running for all actions. Ignored
                               for "dump_data_sources", which by its nature only
                               for "dump_data_sources", which by its nature only
-                              runs for particular actions.
+                              runs for "create".
                           example: [create, prune, compact, check]
                           example: [create, prune, compact, check]
                       run:
                       run:
                           type: array
                           type: array
@@ -1091,8 +1091,10 @@ properties:
                                   - key
                                   - key
                                   - borg
                                   - borg
                           description: |
                           description: |
-                              List of actions for which the commands will be run.
-                              Defaults to running for all actions.
+                              List of actions for which the commands will be
+                              run. Defaults to running for all actions. Ignored
+                              for "dump_data_sources", which by its nature only
+                              runs for "create".
                           example: [create, prune, compact, check]
                           example: [create, prune, compact, check]
                       run:
                       run:
                           type: array
                           type: array

+ 24 - 8
borgmatic/hooks/command.py

@@ -138,9 +138,9 @@ class Before_after_hooks:
        with borgmatic.hooks.command.Before_after_hooks(
        with borgmatic.hooks.command.Before_after_hooks(
            command_hooks=config.get('commands'),
            command_hooks=config.get('commands'),
            before_after='do_stuff',
            before_after='do_stuff',
-           hook_name='myhook',
            umask=config.get('umask'),
            umask=config.get('umask'),
            dry_run=dry_run,
            dry_run=dry_run,
+           hook_name='myhook',
        ):
        ):
             do()
             do()
             some()
             some()
@@ -150,17 +150,27 @@ class Before_after_hooks:
     and "after" command hooks execute after the wrapped code completes.
     and "after" command hooks execute after the wrapped code completes.
     '''
     '''
 
 
-    def __init__(self, command_hooks, before_after, hook_name, umask, dry_run, **context):
+    def __init__(
+        self,
+        command_hooks,
+        before_after,
+        umask,
+        dry_run,
+        hook_name=None,
+        action_names=None,
+        **context,
+    ):
         '''
         '''
-        Given a sequence of command hook configuration dicts, the before/after name, the name of the
-        calling hook, a umask to run commands with, a dry run flag, and any context for the executed
-        commands, save those data points for use below.
+        Given a sequence of command hook configuration dicts, the before/after name, a umask to run
+        commands with, a dry run flag, the name of the calling hook, a sequence of action names, and
+        any context for the executed commands, save those data points for use below.
         '''
         '''
         self.command_hooks = command_hooks
         self.command_hooks = command_hooks
         self.before_after = before_after
         self.before_after = before_after
-        self.hook_name = hook_name
         self.umask = umask
         self.umask = umask
         self.dry_run = dry_run
         self.dry_run = dry_run
+        self.hook_name = hook_name
+        self.action_names = action_names
         self.context = context
         self.context = context
 
 
     def __enter__(self):
     def __enter__(self):
@@ -169,7 +179,10 @@ class Before_after_hooks:
         '''
         '''
         execute_hooks(
         execute_hooks(
             borgmatic.hooks.command.filter_hooks(
             borgmatic.hooks.command.filter_hooks(
-                self.command_hooks, before=self.before_after, hook_name=self.hook_name
+                self.command_hooks,
+                before=self.before_after,
+                hook_name=self.hook_name,
+                action_names=self.action_names,
             ),
             ),
             self.umask,
             self.umask,
             self.dry_run,
             self.dry_run,
@@ -182,7 +195,10 @@ class Before_after_hooks:
         '''
         '''
         execute_hooks(
         execute_hooks(
             borgmatic.hooks.command.filter_hooks(
             borgmatic.hooks.command.filter_hooks(
-                self.command_hooks, after=self.before_after, hook_name=self.hook_name
+                self.command_hooks,
+                after=self.before_after,
+                hook_name=self.hook_name,
+                action_names=self.action_names,
             ),
             ),
             self.umask,
             self.umask,
             self.dry_run,
             self.dry_run,

+ 1 - 1
borgmatic/hooks/data_source/bootstrap.py

@@ -41,9 +41,9 @@ def dump_data_sources(
     with borgmatic.hooks.command.Before_after_hooks(
     with borgmatic.hooks.command.Before_after_hooks(
         command_hooks=config.get('commands'),
         command_hooks=config.get('commands'),
         before_after='dump_data_sources',
         before_after='dump_data_sources',
-        hook_name='bootstrap',
         umask=config.get('umask'),
         umask=config.get('umask'),
         dry_run=dry_run,
         dry_run=dry_run,
+        hook_name='bootstrap',
     ):
     ):
         borgmatic_manifest_path = os.path.join(
         borgmatic_manifest_path = os.path.join(
             borgmatic_runtime_directory, 'bootstrap', 'manifest.json'
             borgmatic_runtime_directory, 'bootstrap', 'manifest.json'

+ 1 - 1
borgmatic/hooks/data_source/btrfs.py

@@ -208,9 +208,9 @@ def dump_data_sources(
     with borgmatic.hooks.command.Before_after_hooks(
     with borgmatic.hooks.command.Before_after_hooks(
         command_hooks=config.get('commands'),
         command_hooks=config.get('commands'),
         before_after='dump_data_sources',
         before_after='dump_data_sources',
-        hook_name='btrfs',
         umask=config.get('umask'),
         umask=config.get('umask'),
         dry_run=dry_run,
         dry_run=dry_run,
+        hook_name='btrfs',
     ):
     ):
         dry_run_label = ' (dry run; not actually snapshotting anything)' if dry_run else ''
         dry_run_label = ' (dry run; not actually snapshotting anything)' if dry_run else ''
         logger.info(f'Snapshotting Btrfs subvolumes{dry_run_label}')
         logger.info(f'Snapshotting Btrfs subvolumes{dry_run_label}')

+ 1 - 1
borgmatic/hooks/data_source/lvm.py

@@ -201,9 +201,9 @@ def dump_data_sources(
     with borgmatic.hooks.command.Before_after_hooks(
     with borgmatic.hooks.command.Before_after_hooks(
         command_hooks=config.get('commands'),
         command_hooks=config.get('commands'),
         function_name='dump_data_sources',
         function_name='dump_data_sources',
-        hook_name='lvm',
         umask=config.get('umask'),
         umask=config.get('umask'),
         dry_run=dry_run,
         dry_run=dry_run,
+        hook_name='lvm',
     ):
     ):
         dry_run_label = ' (dry run; not actually snapshotting anything)' if dry_run else ''
         dry_run_label = ' (dry run; not actually snapshotting anything)' if dry_run else ''
         logger.info(f'Snapshotting LVM logical volumes{dry_run_label}')
         logger.info(f'Snapshotting LVM logical volumes{dry_run_label}')

+ 1 - 1
borgmatic/hooks/data_source/mariadb.py

@@ -246,9 +246,9 @@ def dump_data_sources(
     with borgmatic.hooks.command.Before_after_hooks(
     with borgmatic.hooks.command.Before_after_hooks(
         command_hooks=config.get('commands'),
         command_hooks=config.get('commands'),
         before_after='dump_data_sources',
         before_after='dump_data_sources',
-        hook_name='mariadb',
         umask=config.get('umask'),
         umask=config.get('umask'),
         dry_run=dry_run,
         dry_run=dry_run,
+        hook_name='mariadb',
     ):
     ):
         dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
         dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
         processes = []
         processes = []

+ 1 - 1
borgmatic/hooks/data_source/mongodb.py

@@ -52,9 +52,9 @@ def dump_data_sources(
     with borgmatic.hooks.command.Before_after_hooks(
     with borgmatic.hooks.command.Before_after_hooks(
         command_hooks=config.get('commands'),
         command_hooks=config.get('commands'),
         before_after='dump_data_sources',
         before_after='dump_data_sources',
-        hook_name='mongodb',
         umask=config.get('umask'),
         umask=config.get('umask'),
         dry_run=dry_run,
         dry_run=dry_run,
+        hook_name='mongodb',
     ):
     ):
         dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
         dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
 
 

+ 1 - 1
borgmatic/hooks/data_source/mysql.py

@@ -173,9 +173,9 @@ def dump_data_sources(
     with borgmatic.hooks.command.Before_after_hooks(
     with borgmatic.hooks.command.Before_after_hooks(
         command_hooks=config.get('commands'),
         command_hooks=config.get('commands'),
         before_after='dump_data_sources',
         before_after='dump_data_sources',
-        hook_name='mysql',
         umask=config.get('umask'),
         umask=config.get('umask'),
         dry_run=dry_run,
         dry_run=dry_run,
+        hook_name='mysql',
     ):
     ):
         dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
         dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
         processes = []
         processes = []

+ 1 - 1
borgmatic/hooks/data_source/postgresql.py

@@ -145,9 +145,9 @@ def dump_data_sources(
     with borgmatic.hooks.command.Before_after_hooks(
     with borgmatic.hooks.command.Before_after_hooks(
         command_hooks=config.get('commands'),
         command_hooks=config.get('commands'),
         before_after='dump_data_sources',
         before_after='dump_data_sources',
-        hook_name='postgresql',
         umask=config.get('umask'),
         umask=config.get('umask'),
         dry_run=dry_run,
         dry_run=dry_run,
+        hook_name='postgresql',
     ):
     ):
         dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
         dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
         processes = []
         processes = []

+ 1 - 1
borgmatic/hooks/data_source/sqlite.py

@@ -51,9 +51,9 @@ def dump_data_sources(
     with borgmatic.hooks.command.Before_after_hooks(
     with borgmatic.hooks.command.Before_after_hooks(
         command_hooks=config.get('commands'),
         command_hooks=config.get('commands'),
         before_after='dump_data_sources',
         before_after='dump_data_sources',
-        hook_name='sqlite',
         umask=config.get('umask'),
         umask=config.get('umask'),
         dry_run=dry_run,
         dry_run=dry_run,
+        hook_name='sqlite',
     ):
     ):
         dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
         dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
         processes = []
         processes = []

+ 1 - 1
borgmatic/hooks/data_source/zfs.py

@@ -247,9 +247,9 @@ def dump_data_sources(
     with borgmatic.hooks.command.Before_after_hooks(
     with borgmatic.hooks.command.Before_after_hooks(
         command_hooks=config.get('commands'),
         command_hooks=config.get('commands'),
         before_after='dump_data_sources',
         before_after='dump_data_sources',
-        hook_name='zfs',
         umask=config.get('umask'),
         umask=config.get('umask'),
         dry_run=dry_run,
         dry_run=dry_run,
+        hook_name='zfs',
     ):
     ):
         dry_run_label = ' (dry run; not actually snapshotting anything)' if dry_run else ''
         dry_run_label = ' (dry run; not actually snapshotting anything)' if dry_run else ''
         logger.info(f'Snapshotting ZFS datasets{dry_run_label}')
         logger.info(f'Snapshotting ZFS datasets{dry_run_label}')

+ 0 - 10
tests/unit/actions/test_check.py

@@ -1405,7 +1405,6 @@ def test_run_check_checks_archives_for_configured_repository():
     flexmock(module).should_receive('make_check_time_path')
     flexmock(module).should_receive('make_check_time_path')
     flexmock(module).should_receive('write_check_time')
     flexmock(module).should_receive('write_check_time')
     flexmock(module.borgmatic.borg.extract).should_receive('extract_last_archive_dry_run').never()
     flexmock(module.borgmatic.borg.extract).should_receive('extract_last_archive_dry_run').never()
-    flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
     check_arguments = flexmock(
     check_arguments = flexmock(
         repository=None,
         repository=None,
         progress=flexmock(),
         progress=flexmock(),
@@ -1419,7 +1418,6 @@ def test_run_check_checks_archives_for_configured_repository():
         config_filename='test.yaml',
         config_filename='test.yaml',
         repository={'path': 'repo'},
         repository={'path': 'repo'},
         config={'repositories': ['repo']},
         config={'repositories': ['repo']},
-        hook_context={},
         local_borg_version=None,
         local_borg_version=None,
         check_arguments=check_arguments,
         check_arguments=check_arguments,
         global_arguments=global_arguments,
         global_arguments=global_arguments,
@@ -1441,7 +1439,6 @@ def test_run_check_runs_configured_extract_check():
     flexmock(module.borgmatic.borg.extract).should_receive('extract_last_archive_dry_run').once()
     flexmock(module.borgmatic.borg.extract).should_receive('extract_last_archive_dry_run').once()
     flexmock(module).should_receive('make_check_time_path')
     flexmock(module).should_receive('make_check_time_path')
     flexmock(module).should_receive('write_check_time')
     flexmock(module).should_receive('write_check_time')
-    flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
     check_arguments = flexmock(
     check_arguments = flexmock(
         repository=None,
         repository=None,
         progress=flexmock(),
         progress=flexmock(),
@@ -1455,7 +1452,6 @@ def test_run_check_runs_configured_extract_check():
         config_filename='test.yaml',
         config_filename='test.yaml',
         repository={'path': 'repo'},
         repository={'path': 'repo'},
         config={'repositories': ['repo']},
         config={'repositories': ['repo']},
-        hook_context={},
         local_borg_version=None,
         local_borg_version=None,
         check_arguments=check_arguments,
         check_arguments=check_arguments,
         global_arguments=global_arguments,
         global_arguments=global_arguments,
@@ -1480,7 +1476,6 @@ def test_run_check_runs_configured_spot_check():
     flexmock(module.borgmatic.actions.check).should_receive('spot_check').once()
     flexmock(module.borgmatic.actions.check).should_receive('spot_check').once()
     flexmock(module).should_receive('make_check_time_path')
     flexmock(module).should_receive('make_check_time_path')
     flexmock(module).should_receive('write_check_time')
     flexmock(module).should_receive('write_check_time')
-    flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
     check_arguments = flexmock(
     check_arguments = flexmock(
         repository=None,
         repository=None,
         progress=flexmock(),
         progress=flexmock(),
@@ -1494,7 +1489,6 @@ def test_run_check_runs_configured_spot_check():
         config_filename='test.yaml',
         config_filename='test.yaml',
         repository={'path': 'repo'},
         repository={'path': 'repo'},
         config={'repositories': ['repo']},
         config={'repositories': ['repo']},
-        hook_context={},
         local_borg_version=None,
         local_borg_version=None,
         check_arguments=check_arguments,
         check_arguments=check_arguments,
         global_arguments=global_arguments,
         global_arguments=global_arguments,
@@ -1516,7 +1510,6 @@ def test_run_check_without_checks_runs_nothing_except_hooks():
     flexmock(module).should_receive('make_check_time_path')
     flexmock(module).should_receive('make_check_time_path')
     flexmock(module).should_receive('write_check_time').never()
     flexmock(module).should_receive('write_check_time').never()
     flexmock(module.borgmatic.borg.extract).should_receive('extract_last_archive_dry_run').never()
     flexmock(module.borgmatic.borg.extract).should_receive('extract_last_archive_dry_run').never()
-    flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
     check_arguments = flexmock(
     check_arguments = flexmock(
         repository=None,
         repository=None,
         progress=flexmock(),
         progress=flexmock(),
@@ -1530,7 +1523,6 @@ def test_run_check_without_checks_runs_nothing_except_hooks():
         config_filename='test.yaml',
         config_filename='test.yaml',
         repository={'path': 'repo'},
         repository={'path': 'repo'},
         config={'repositories': ['repo']},
         config={'repositories': ['repo']},
-        hook_context={},
         local_borg_version=None,
         local_borg_version=None,
         check_arguments=check_arguments,
         check_arguments=check_arguments,
         global_arguments=global_arguments,
         global_arguments=global_arguments,
@@ -1569,7 +1561,6 @@ def test_run_check_checks_archives_in_selected_repository():
         config_filename='test.yaml',
         config_filename='test.yaml',
         repository={'path': 'repo'},
         repository={'path': 'repo'},
         config={'repositories': ['repo']},
         config={'repositories': ['repo']},
-        hook_context={},
         local_borg_version=None,
         local_borg_version=None,
         check_arguments=check_arguments,
         check_arguments=check_arguments,
         global_arguments=global_arguments,
         global_arguments=global_arguments,
@@ -1597,7 +1588,6 @@ def test_run_check_bails_if_repository_does_not_match():
         config_filename='test.yaml',
         config_filename='test.yaml',
         repository={'path': 'repo'},
         repository={'path': 'repo'},
         config={'repositories': ['repo']},
         config={'repositories': ['repo']},
-        hook_context={},
         local_borg_version=None,
         local_borg_version=None,
         check_arguments=check_arguments,
         check_arguments=check_arguments,
         global_arguments=global_arguments,
         global_arguments=global_arguments,

+ 0 - 4
tests/unit/actions/test_compact.py

@@ -8,7 +8,6 @@ def test_compact_actions_calls_hooks_for_configured_repository():
     flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
     flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
     flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
     flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
     flexmock(module.borgmatic.borg.compact).should_receive('compact_segments').once()
     flexmock(module.borgmatic.borg.compact).should_receive('compact_segments').once()
-    flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
     compact_arguments = flexmock(
     compact_arguments = flexmock(
         repository=None, progress=flexmock(), cleanup_commits=flexmock(), threshold=flexmock()
         repository=None, progress=flexmock(), cleanup_commits=flexmock(), threshold=flexmock()
     )
     )
@@ -18,7 +17,6 @@ def test_compact_actions_calls_hooks_for_configured_repository():
         config_filename='test.yaml',
         config_filename='test.yaml',
         repository={'path': 'repo'},
         repository={'path': 'repo'},
         config={},
         config={},
-        hook_context={},
         local_borg_version=None,
         local_borg_version=None,
         compact_arguments=compact_arguments,
         compact_arguments=compact_arguments,
         global_arguments=global_arguments,
         global_arguments=global_arguments,
@@ -44,7 +42,6 @@ def test_compact_runs_with_selected_repository():
         config_filename='test.yaml',
         config_filename='test.yaml',
         repository={'path': 'repo'},
         repository={'path': 'repo'},
         config={},
         config={},
-        hook_context={},
         local_borg_version=None,
         local_borg_version=None,
         compact_arguments=compact_arguments,
         compact_arguments=compact_arguments,
         global_arguments=global_arguments,
         global_arguments=global_arguments,
@@ -70,7 +67,6 @@ def test_compact_bails_if_repository_does_not_match():
         config_filename='test.yaml',
         config_filename='test.yaml',
         repository={'path': 'repo'},
         repository={'path': 'repo'},
         config={},
         config={},
-        hook_context={},
         local_borg_version=None,
         local_borg_version=None,
         compact_arguments=compact_arguments,
         compact_arguments=compact_arguments,
         global_arguments=global_arguments,
         global_arguments=global_arguments,

+ 0 - 7
tests/unit/actions/test_create.py

@@ -424,7 +424,6 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository():
         flexmock()
         flexmock()
     )
     )
     flexmock(module.borgmatic.borg.create).should_receive('create_archive').once()
     flexmock(module.borgmatic.borg.create).should_receive('create_archive').once()
-    flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
     flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return({})
     flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return({})
     flexmock(module.borgmatic.hooks.dispatch).should_receive(
     flexmock(module.borgmatic.hooks.dispatch).should_receive(
         'call_hooks_even_if_unconfigured'
         'call_hooks_even_if_unconfigured'
@@ -447,7 +446,6 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository():
             repository={'path': 'repo'},
             repository={'path': 'repo'},
             config={},
             config={},
             config_paths=['/tmp/test.yaml'],
             config_paths=['/tmp/test.yaml'],
-            hook_context={},
             local_borg_version=None,
             local_borg_version=None,
             create_arguments=create_arguments,
             create_arguments=create_arguments,
             global_arguments=global_arguments,
             global_arguments=global_arguments,
@@ -467,7 +465,6 @@ def test_run_create_runs_with_selected_repository():
         flexmock()
         flexmock()
     )
     )
     flexmock(module.borgmatic.borg.create).should_receive('create_archive').once()
     flexmock(module.borgmatic.borg.create).should_receive('create_archive').once()
-    flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
     flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return({})
     flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return({})
     flexmock(module.borgmatic.hooks.dispatch).should_receive(
     flexmock(module.borgmatic.hooks.dispatch).should_receive(
         'call_hooks_even_if_unconfigured'
         'call_hooks_even_if_unconfigured'
@@ -490,7 +487,6 @@ def test_run_create_runs_with_selected_repository():
             repository={'path': 'repo'},
             repository={'path': 'repo'},
             config={},
             config={},
             config_paths=['/tmp/test.yaml'],
             config_paths=['/tmp/test.yaml'],
-            hook_context={},
             local_borg_version=None,
             local_borg_version=None,
             create_arguments=create_arguments,
             create_arguments=create_arguments,
             global_arguments=global_arguments,
             global_arguments=global_arguments,
@@ -523,7 +519,6 @@ def test_run_create_bails_if_repository_does_not_match():
             repository='repo',
             repository='repo',
             config={},
             config={},
             config_paths=['/tmp/test.yaml'],
             config_paths=['/tmp/test.yaml'],
-            hook_context={},
             local_borg_version=None,
             local_borg_version=None,
             create_arguments=create_arguments,
             create_arguments=create_arguments,
             global_arguments=global_arguments,
             global_arguments=global_arguments,
@@ -547,7 +542,6 @@ def test_run_create_produces_json():
     )
     )
     parsed_json = flexmock()
     parsed_json = flexmock()
     flexmock(module.borgmatic.actions.json).should_receive('parse_json').and_return(parsed_json)
     flexmock(module.borgmatic.actions.json).should_receive('parse_json').and_return(parsed_json)
-    flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
     flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return({})
     flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return({})
     flexmock(module.borgmatic.hooks.dispatch).should_receive(
     flexmock(module.borgmatic.hooks.dispatch).should_receive(
         'call_hooks_even_if_unconfigured'
         'call_hooks_even_if_unconfigured'
@@ -570,7 +564,6 @@ def test_run_create_produces_json():
             repository={'path': 'repo'},
             repository={'path': 'repo'},
             config={},
             config={},
             config_paths=['/tmp/test.yaml'],
             config_paths=['/tmp/test.yaml'],
-            hook_context={},
             local_borg_version=None,
             local_borg_version=None,
             create_arguments=create_arguments,
             create_arguments=create_arguments,
             global_arguments=global_arguments,
             global_arguments=global_arguments,

+ 0 - 2
tests/unit/actions/test_extract.py

@@ -7,7 +7,6 @@ def test_run_extract_calls_hooks():
     flexmock(module.logger).answer = lambda message: None
     flexmock(module.logger).answer = lambda message: None
     flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
     flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
     flexmock(module.borgmatic.borg.extract).should_receive('extract_archive')
     flexmock(module.borgmatic.borg.extract).should_receive('extract_archive')
-    flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
     extract_arguments = flexmock(
     extract_arguments = flexmock(
         paths=flexmock(),
         paths=flexmock(),
         progress=flexmock(),
         progress=flexmock(),
@@ -22,7 +21,6 @@ def test_run_extract_calls_hooks():
         config_filename='test.yaml',
         config_filename='test.yaml',
         repository={'path': 'repo'},
         repository={'path': 'repo'},
         config={'repositories': ['repo']},
         config={'repositories': ['repo']},
-        hook_context={},
         local_borg_version=None,
         local_borg_version=None,
         extract_arguments=extract_arguments,
         extract_arguments=extract_arguments,
         global_arguments=global_arguments,
         global_arguments=global_arguments,

+ 0 - 4
tests/unit/actions/test_prune.py

@@ -7,7 +7,6 @@ def test_run_prune_calls_hooks_for_configured_repository():
     flexmock(module.logger).answer = lambda message: None
     flexmock(module.logger).answer = lambda message: None
     flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
     flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
     flexmock(module.borgmatic.borg.prune).should_receive('prune_archives').once()
     flexmock(module.borgmatic.borg.prune).should_receive('prune_archives').once()
-    flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
     prune_arguments = flexmock(repository=None, stats=flexmock(), list_archives=flexmock())
     prune_arguments = flexmock(repository=None, stats=flexmock(), list_archives=flexmock())
     global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
     global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
 
 
@@ -15,7 +14,6 @@ def test_run_prune_calls_hooks_for_configured_repository():
         config_filename='test.yaml',
         config_filename='test.yaml',
         repository={'path': 'repo'},
         repository={'path': 'repo'},
         config={},
         config={},
-        hook_context={},
         local_borg_version=None,
         local_borg_version=None,
         prune_arguments=prune_arguments,
         prune_arguments=prune_arguments,
         global_arguments=global_arguments,
         global_arguments=global_arguments,
@@ -38,7 +36,6 @@ def test_run_prune_runs_with_selected_repository():
         config_filename='test.yaml',
         config_filename='test.yaml',
         repository={'path': 'repo'},
         repository={'path': 'repo'},
         config={},
         config={},
-        hook_context={},
         local_borg_version=None,
         local_borg_version=None,
         prune_arguments=prune_arguments,
         prune_arguments=prune_arguments,
         global_arguments=global_arguments,
         global_arguments=global_arguments,
@@ -61,7 +58,6 @@ def test_run_prune_bails_if_repository_does_not_match():
         config_filename='test.yaml',
         config_filename='test.yaml',
         repository='repo',
         repository='repo',
         config={},
         config={},
-        hook_context={},
         local_borg_version=None,
         local_borg_version=None,
         prune_arguments=prune_arguments,
         prune_arguments=prune_arguments,
         global_arguments=global_arguments,
         global_arguments=global_arguments,

+ 134 - 69
tests/unit/commands/test_borgmatic.py

@@ -30,6 +30,7 @@ def test_get_skip_actions_uses_config_and_arguments(config, arguments, expected_
 def test_run_configuration_runs_actions_for_each_repository():
 def test_run_configuration_runs_actions_for_each_repository():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     expected_results = [flexmock(), flexmock()]
     expected_results = [flexmock(), flexmock()]
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
@@ -37,7 +38,7 @@ def test_run_configuration_runs_actions_for_each_repository():
         expected_results[1:]
         expected_results[1:]
     )
     )
     config = {'repositories': [{'path': 'foo'}, {'path': 'bar'}]}
     config = {'repositories': [{'path': 'foo'}, {'path': 'bar'}]}
-    arguments = {'global': flexmock(monitoring_verbosity=1)}
+    arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False)}
 
 
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
 
 
@@ -47,11 +48,12 @@ def test_run_configuration_runs_actions_for_each_repository():
 def test_run_configuration_with_skip_actions_does_not_raise():
 def test_run_configuration_with_skip_actions_does_not_raise():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return(['compact'])
     flexmock(module).should_receive('get_skip_actions').and_return(['compact'])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_return(flexmock()).and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_return(flexmock()).and_return(flexmock())
     config = {'repositories': [{'path': 'foo'}, {'path': 'bar'}], 'skip_actions': ['compact']}
     config = {'repositories': [{'path': 'foo'}, {'path': 'bar'}], 'skip_actions': ['compact']}
-    arguments = {'global': flexmock(monitoring_verbosity=1)}
+    arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False)}
 
 
     list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
     list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
 
 
@@ -59,8 +61,8 @@ def test_run_configuration_with_skip_actions_does_not_raise():
 def test_run_configuration_with_invalid_borg_version_errors():
 def test_run_configuration_with_invalid_borg_version_errors():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_raise(ValueError)
     flexmock(module.borg_version).should_receive('local_borg_version').and_raise(ValueError)
-    flexmock(module.command).should_receive('execute_hook').never()
     flexmock(module.dispatch).should_receive('call_hooks').never()
     flexmock(module.dispatch).should_receive('call_hooks').never()
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').never()
     flexmock(module).should_receive('run_actions').never()
@@ -73,6 +75,7 @@ def test_run_configuration_with_invalid_borg_version_errors():
 def test_run_configuration_logs_monitor_start_error():
 def test_run_configuration_logs_monitor_start_error():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.dispatch).should_receive('call_hooks').and_raise(OSError).and_return(
     flexmock(module.dispatch).should_receive('call_hooks').and_raise(OSError).and_return(
         None
         None
@@ -81,6 +84,8 @@ def test_run_configuration_logs_monitor_start_error():
     flexmock(module).should_receive('log_error_records').and_return(expected_results)
     flexmock(module).should_receive('log_error_records').and_return(expected_results)
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').never()
     flexmock(module).should_receive('run_actions').never()
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {'repositories': [{'path': 'foo'}]}
     config = {'repositories': [{'path': 'foo'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
 
 
@@ -92,6 +97,7 @@ def test_run_configuration_logs_monitor_start_error():
 def test_run_configuration_bails_for_monitor_start_soft_failure():
 def test_run_configuration_bails_for_monitor_start_soft_failure():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
     error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
     flexmock(module.dispatch).should_receive('call_hooks').and_raise(error).and_return(None)
     flexmock(module.dispatch).should_receive('call_hooks').and_raise(error).and_return(None)
@@ -109,13 +115,15 @@ def test_run_configuration_bails_for_monitor_start_soft_failure():
 def test_run_configuration_logs_actions_error():
 def test_run_configuration_logs_actions_error():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
-    flexmock(module.command).should_receive('execute_hook')
     flexmock(module.dispatch).should_receive('call_hooks')
     flexmock(module.dispatch).should_receive('call_hooks')
     expected_results = [flexmock()]
     expected_results = [flexmock()]
     flexmock(module).should_receive('log_error_records').and_return(expected_results)
     flexmock(module).should_receive('log_error_records').and_return(expected_results)
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_raise(OSError)
     flexmock(module).should_receive('run_actions').and_raise(OSError)
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {'repositories': [{'path': 'foo'}]}
     config = {'repositories': [{'path': 'foo'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False)}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False)}
 
 
@@ -127,6 +135,7 @@ def test_run_configuration_logs_actions_error():
 def test_run_configuration_skips_remaining_actions_for_actions_soft_failure_but_still_runs_next_repository_actions():
 def test_run_configuration_skips_remaining_actions_for_actions_soft_failure_but_still_runs_next_repository_actions():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.dispatch).should_receive('call_hooks').times(5)
     flexmock(module.dispatch).should_receive('call_hooks').times(5)
     error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
     error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
@@ -146,6 +155,7 @@ def test_run_configuration_skips_remaining_actions_for_actions_soft_failure_but_
 def test_run_configuration_logs_monitor_log_error():
 def test_run_configuration_logs_monitor_log_error():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return(
     flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return(
         None
         None
@@ -154,6 +164,8 @@ def test_run_configuration_logs_monitor_log_error():
     flexmock(module).should_receive('log_error_records').and_return(expected_results)
     flexmock(module).should_receive('log_error_records').and_return(expected_results)
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_return([])
     flexmock(module).should_receive('run_actions').and_return([])
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {'repositories': [{'path': 'foo'}]}
     config = {'repositories': [{'path': 'foo'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
 
 
@@ -165,6 +177,7 @@ def test_run_configuration_logs_monitor_log_error():
 def test_run_configuration_still_pings_monitor_for_monitor_log_soft_failure():
 def test_run_configuration_still_pings_monitor_for_monitor_log_soft_failure():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
     error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
     flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return(
     flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return(
@@ -185,6 +198,7 @@ def test_run_configuration_still_pings_monitor_for_monitor_log_soft_failure():
 def test_run_configuration_logs_monitor_finish_error():
 def test_run_configuration_logs_monitor_finish_error():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return(
     flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return(
         None
         None
@@ -193,6 +207,8 @@ def test_run_configuration_logs_monitor_finish_error():
     flexmock(module).should_receive('log_error_records').and_return(expected_results)
     flexmock(module).should_receive('log_error_records').and_return(expected_results)
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_return([])
     flexmock(module).should_receive('run_actions').and_return([])
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {'repositories': [{'path': 'foo'}]}
     config = {'repositories': [{'path': 'foo'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
 
 
@@ -204,6 +220,7 @@ def test_run_configuration_logs_monitor_finish_error():
 def test_run_configuration_bails_for_monitor_finish_soft_failure():
 def test_run_configuration_bails_for_monitor_finish_soft_failure():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
     error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
     flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return(
     flexmock(module.dispatch).should_receive('call_hooks').and_return(None).and_return(
@@ -224,6 +241,7 @@ def test_run_configuration_bails_for_monitor_finish_soft_failure():
 def test_run_configuration_does_not_call_monitoring_hooks_if_monitoring_hooks_are_disabled():
 def test_run_configuration_does_not_call_monitoring_hooks_if_monitoring_hooks_are_disabled():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(module.DISABLED)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(module.DISABLED)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
 
 
     flexmock(module.dispatch).should_receive('call_hooks').never()
     flexmock(module.dispatch).should_receive('call_hooks').never()
@@ -239,8 +257,10 @@ def test_run_configuration_does_not_call_monitoring_hooks_if_monitoring_hooks_ar
 def test_run_configuration_logs_on_error_hook_error():
 def test_run_configuration_logs_on_error_hook_error():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
-    flexmock(module.command).should_receive('execute_hook').and_raise(OSError)
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks').and_raise(OSError)
     expected_results = [flexmock(), flexmock()]
     expected_results = [flexmock(), flexmock()]
     flexmock(module).should_receive('log_error_records').and_return(
     flexmock(module).should_receive('log_error_records').and_return(
         expected_results[:1]
         expected_results[:1]
@@ -258,9 +278,11 @@ def test_run_configuration_logs_on_error_hook_error():
 def test_run_configuration_bails_for_on_error_hook_soft_failure():
 def test_run_configuration_bails_for_on_error_hook_soft_failure():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
     error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
-    flexmock(module.command).should_receive('execute_hook').and_raise(error)
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks').and_raise(error)
     expected_results = [flexmock()]
     expected_results = [flexmock()]
     flexmock(module).should_receive('log_error_records').and_return(expected_results)
     flexmock(module).should_receive('log_error_records').and_return(expected_results)
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
@@ -277,14 +299,18 @@ def test_run_configuration_retries_soft_error():
     # Run action first fails, second passes.
     # Run action first fails, second passes.
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
-    flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_raise(OSError).and_return([])
     flexmock(module).should_receive('run_actions').and_raise(OSError).and_return([])
     flexmock(module).should_receive('log_error_records').and_return([flexmock()]).once()
     flexmock(module).should_receive('log_error_records').and_return([flexmock()]).once()
+    flexmock(module.command).should_receive('filter_hooks').never()
+    flexmock(module.command).should_receive('execute_hooks').never()
     config = {'repositories': [{'path': 'foo'}], 'retries': 1}
     config = {'repositories': [{'path': 'foo'}], 'retries': 1}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
+
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
+
     assert results == []
     assert results == []
 
 
 
 
@@ -292,8 +318,8 @@ def test_run_configuration_retries_hard_error():
     # Run action fails twice.
     # Run action fails twice.
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
-    flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_raise(OSError).times(2)
     flexmock(module).should_receive('run_actions').and_raise(OSError).times(2)
     flexmock(module).should_receive('log_error_records').with_args(
     flexmock(module).should_receive('log_error_records').with_args(
@@ -307,17 +333,21 @@ def test_run_configuration_retries_hard_error():
         'Error running actions for repository',
         'Error running actions for repository',
         OSError,
         OSError,
     ).and_return(error_logs)
     ).and_return(error_logs)
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {'repositories': [{'path': 'foo'}], 'retries': 1}
     config = {'repositories': [{'path': 'foo'}], 'retries': 1}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
+
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
+
     assert results == error_logs
     assert results == error_logs
 
 
 
 
-def test_run_configuration_repos_ordered():
+def test_run_configuration_retries_repositories_in_order():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
-    flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_raise(OSError).times(2)
     flexmock(module).should_receive('run_actions').and_raise(OSError).times(2)
     expected_results = [flexmock(), flexmock()]
     expected_results = [flexmock(), flexmock()]
@@ -327,17 +357,21 @@ def test_run_configuration_repos_ordered():
     flexmock(module).should_receive('log_error_records').with_args(
     flexmock(module).should_receive('log_error_records').with_args(
         'Error running actions for repository', OSError
         'Error running actions for repository', OSError
     ).and_return(expected_results[1:]).ordered()
     ).and_return(expected_results[1:]).ordered()
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {'repositories': [{'path': 'foo'}, {'path': 'bar'}]}
     config = {'repositories': [{'path': 'foo'}, {'path': 'bar'}]}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
+
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
+
     assert results == expected_results
     assert results == expected_results
 
 
 
 
 def test_run_configuration_retries_round_robin():
 def test_run_configuration_retries_round_robin():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
-    flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_raise(OSError).times(4)
     flexmock(module).should_receive('run_actions').and_raise(OSError).times(4)
     flexmock(module).should_receive('log_error_records').with_args(
     flexmock(module).should_receive('log_error_records').with_args(
@@ -360,20 +394,24 @@ def test_run_configuration_retries_round_robin():
     flexmock(module).should_receive('log_error_records').with_args(
     flexmock(module).should_receive('log_error_records').with_args(
         'Error running actions for repository', OSError
         'Error running actions for repository', OSError
     ).and_return(bar_error_logs).ordered()
     ).and_return(bar_error_logs).ordered()
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {
     config = {
         'repositories': [{'path': 'foo'}, {'path': 'bar'}],
         'repositories': [{'path': 'foo'}, {'path': 'bar'}],
         'retries': 1,
         'retries': 1,
     }
     }
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
+
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
+
     assert results == foo_error_logs + bar_error_logs
     assert results == foo_error_logs + bar_error_logs
 
 
 
 
-def test_run_configuration_retries_one_passes():
+def test_run_configuration_with_one_retry():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
-    flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_raise(OSError).and_raise(OSError).and_return(
     flexmock(module).should_receive('run_actions').and_raise(OSError).and_raise(OSError).and_return(
         []
         []
@@ -394,20 +432,24 @@ def test_run_configuration_retries_one_passes():
     flexmock(module).should_receive('log_error_records').with_args(
     flexmock(module).should_receive('log_error_records').with_args(
         'Error running actions for repository', OSError
         'Error running actions for repository', OSError
     ).and_return(error_logs).ordered()
     ).and_return(error_logs).ordered()
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {
     config = {
         'repositories': [{'path': 'foo'}, {'path': 'bar'}],
         'repositories': [{'path': 'foo'}, {'path': 'bar'}],
         'retries': 1,
         'retries': 1,
     }
     }
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
+
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
+
     assert results == error_logs
     assert results == error_logs
 
 
 
 
-def test_run_configuration_retry_wait():
+def test_run_configuration_with_retry_wait_does_backoff_after_each_retry():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
-    flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_raise(OSError).times(4)
     flexmock(module).should_receive('run_actions').and_raise(OSError).times(4)
     flexmock(module).should_receive('log_error_records').with_args(
     flexmock(module).should_receive('log_error_records').with_args(
@@ -438,21 +480,25 @@ def test_run_configuration_retry_wait():
     flexmock(module).should_receive('log_error_records').with_args(
     flexmock(module).should_receive('log_error_records').with_args(
         'Error running actions for repository', OSError
         'Error running actions for repository', OSError
     ).and_return(error_logs).ordered()
     ).and_return(error_logs).ordered()
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {
     config = {
         'repositories': [{'path': 'foo'}],
         'repositories': [{'path': 'foo'}],
         'retries': 3,
         'retries': 3,
         'retry_wait': 10,
         'retry_wait': 10,
     }
     }
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
+
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
+
     assert results == error_logs
     assert results == error_logs
 
 
 
 
-def test_run_configuration_retries_timeout_multiple_repos():
+def test_run_configuration_with_multiple_repositories_retries_with_timeout():
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
     flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
-    flexmock(module.command).should_receive('execute_hook')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_actions').and_raise(OSError).and_raise(OSError).and_return(
     flexmock(module).should_receive('run_actions').and_raise(OSError).and_raise(OSError).and_return(
         []
         []
@@ -479,20 +525,24 @@ def test_run_configuration_retries_timeout_multiple_repos():
     flexmock(module).should_receive('log_error_records').with_args(
     flexmock(module).should_receive('log_error_records').with_args(
         'Error running actions for repository', OSError
         'Error running actions for repository', OSError
     ).and_return(error_logs).ordered()
     ).and_return(error_logs).ordered()
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     config = {
     config = {
         'repositories': [{'path': 'foo'}, {'path': 'bar'}],
         'repositories': [{'path': 'foo'}, {'path': 'bar'}],
         'retries': 1,
         'retries': 1,
         'retry_wait': 10,
         'retry_wait': 10,
     }
     }
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
     arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
+
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
     results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
+
     assert results == error_logs
     assert results == error_logs
 
 
 
 
 def test_run_actions_runs_repo_create():
 def test_run_actions_runs_repo_create():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.repo_create).should_receive('run_repo_create').once()
     flexmock(borgmatic.actions.repo_create).should_receive('run_repo_create').once()
 
 
     tuple(
     tuple(
@@ -515,19 +565,13 @@ def test_run_actions_runs_repo_create():
 def test_run_actions_adds_label_file_to_hook_context():
 def test_run_actions_adds_label_file_to_hook_context():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     expected = flexmock()
     expected = flexmock()
     flexmock(borgmatic.actions.create).should_receive('run_create').with_args(
     flexmock(borgmatic.actions.create).should_receive('run_create').with_args(
         config_filename=object,
         config_filename=object,
         repository={'path': 'repo', 'label': 'my repo'},
         repository={'path': 'repo', 'label': 'my repo'},
         config={'repositories': []},
         config={'repositories': []},
         config_paths=[],
         config_paths=[],
-        hook_context={
-            'repository_label': 'my repo',
-            'log_file': '',
-            'repositories': '',
-            'repository': 'repo',
-        },
         local_borg_version=object,
         local_borg_version=object,
         create_arguments=object,
         create_arguments=object,
         global_arguments=object,
         global_arguments=object,
@@ -554,19 +598,13 @@ def test_run_actions_adds_label_file_to_hook_context():
 def test_run_actions_adds_log_file_to_hook_context():
 def test_run_actions_adds_log_file_to_hook_context():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     expected = flexmock()
     expected = flexmock()
     flexmock(borgmatic.actions.create).should_receive('run_create').with_args(
     flexmock(borgmatic.actions.create).should_receive('run_create').with_args(
         config_filename=object,
         config_filename=object,
         repository={'path': 'repo'},
         repository={'path': 'repo'},
         config={'repositories': []},
         config={'repositories': []},
         config_paths=[],
         config_paths=[],
-        hook_context={
-            'repository_label': '',
-            'log_file': 'foo',
-            'repositories': '',
-            'repository': 'repo',
-        },
         local_borg_version=object,
         local_borg_version=object,
         create_arguments=object,
         create_arguments=object,
         global_arguments=object,
         global_arguments=object,
@@ -593,7 +631,7 @@ def test_run_actions_adds_log_file_to_hook_context():
 def test_run_actions_runs_transfer():
 def test_run_actions_runs_transfer():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.transfer).should_receive('run_transfer').once()
     flexmock(borgmatic.actions.transfer).should_receive('run_transfer').once()
 
 
     tuple(
     tuple(
@@ -613,7 +651,7 @@ def test_run_actions_runs_transfer():
 def test_run_actions_runs_create():
 def test_run_actions_runs_create():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     expected = flexmock()
     expected = flexmock()
     flexmock(borgmatic.actions.create).should_receive('run_create').and_yield(expected).once()
     flexmock(borgmatic.actions.create).should_receive('run_create').and_yield(expected).once()
 
 
@@ -635,7 +673,7 @@ def test_run_actions_runs_create():
 def test_run_actions_with_skip_actions_skips_create():
 def test_run_actions_with_skip_actions_skips_create():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return(['create'])
     flexmock(module).should_receive('get_skip_actions').and_return(['create'])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.create).should_receive('run_create').never()
     flexmock(borgmatic.actions.create).should_receive('run_create').never()
 
 
     tuple(
     tuple(
@@ -655,7 +693,7 @@ def test_run_actions_with_skip_actions_skips_create():
 def test_run_actions_runs_prune():
 def test_run_actions_runs_prune():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.prune).should_receive('run_prune').once()
     flexmock(borgmatic.actions.prune).should_receive('run_prune').once()
 
 
     tuple(
     tuple(
@@ -675,7 +713,7 @@ def test_run_actions_runs_prune():
 def test_run_actions_with_skip_actions_skips_prune():
 def test_run_actions_with_skip_actions_skips_prune():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return(['prune'])
     flexmock(module).should_receive('get_skip_actions').and_return(['prune'])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.prune).should_receive('run_prune').never()
     flexmock(borgmatic.actions.prune).should_receive('run_prune').never()
 
 
     tuple(
     tuple(
@@ -695,7 +733,7 @@ def test_run_actions_with_skip_actions_skips_prune():
 def test_run_actions_runs_compact():
 def test_run_actions_runs_compact():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.compact).should_receive('run_compact').once()
     flexmock(borgmatic.actions.compact).should_receive('run_compact').once()
 
 
     tuple(
     tuple(
@@ -715,7 +753,7 @@ def test_run_actions_runs_compact():
 def test_run_actions_with_skip_actions_skips_compact():
 def test_run_actions_with_skip_actions_skips_compact():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return(['compact'])
     flexmock(module).should_receive('get_skip_actions').and_return(['compact'])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.compact).should_receive('run_compact').never()
     flexmock(borgmatic.actions.compact).should_receive('run_compact').never()
 
 
     tuple(
     tuple(
@@ -735,7 +773,7 @@ def test_run_actions_with_skip_actions_skips_compact():
 def test_run_actions_runs_check_when_repository_enabled_for_checks():
 def test_run_actions_runs_check_when_repository_enabled_for_checks():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.checks).should_receive('repository_enabled_for_checks').and_return(True)
     flexmock(module.checks).should_receive('repository_enabled_for_checks').and_return(True)
     flexmock(borgmatic.actions.check).should_receive('run_check').once()
     flexmock(borgmatic.actions.check).should_receive('run_check').once()
 
 
@@ -756,7 +794,7 @@ def test_run_actions_runs_check_when_repository_enabled_for_checks():
 def test_run_actions_skips_check_when_repository_not_enabled_for_checks():
 def test_run_actions_skips_check_when_repository_not_enabled_for_checks():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.checks).should_receive('repository_enabled_for_checks').and_return(False)
     flexmock(module.checks).should_receive('repository_enabled_for_checks').and_return(False)
     flexmock(borgmatic.actions.check).should_receive('run_check').never()
     flexmock(borgmatic.actions.check).should_receive('run_check').never()
 
 
@@ -777,7 +815,7 @@ def test_run_actions_skips_check_when_repository_not_enabled_for_checks():
 def test_run_actions_with_skip_actions_skips_check():
 def test_run_actions_with_skip_actions_skips_check():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return(['check'])
     flexmock(module).should_receive('get_skip_actions').and_return(['check'])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(module.checks).should_receive('repository_enabled_for_checks').and_return(True)
     flexmock(module.checks).should_receive('repository_enabled_for_checks').and_return(True)
     flexmock(borgmatic.actions.check).should_receive('run_check').never()
     flexmock(borgmatic.actions.check).should_receive('run_check').never()
 
 
@@ -798,7 +836,7 @@ def test_run_actions_with_skip_actions_skips_check():
 def test_run_actions_runs_extract():
 def test_run_actions_runs_extract():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.extract).should_receive('run_extract').once()
     flexmock(borgmatic.actions.extract).should_receive('run_extract').once()
 
 
     tuple(
     tuple(
@@ -818,7 +856,7 @@ def test_run_actions_runs_extract():
 def test_run_actions_runs_export_tar():
 def test_run_actions_runs_export_tar():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.export_tar).should_receive('run_export_tar').once()
     flexmock(borgmatic.actions.export_tar).should_receive('run_export_tar').once()
 
 
     tuple(
     tuple(
@@ -838,7 +876,7 @@ def test_run_actions_runs_export_tar():
 def test_run_actions_runs_mount():
 def test_run_actions_runs_mount():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.mount).should_receive('run_mount').once()
     flexmock(borgmatic.actions.mount).should_receive('run_mount').once()
 
 
     tuple(
     tuple(
@@ -858,7 +896,7 @@ def test_run_actions_runs_mount():
 def test_run_actions_runs_restore():
 def test_run_actions_runs_restore():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.restore).should_receive('run_restore').once()
     flexmock(borgmatic.actions.restore).should_receive('run_restore').once()
 
 
     tuple(
     tuple(
@@ -878,7 +916,7 @@ def test_run_actions_runs_restore():
 def test_run_actions_runs_repo_list():
 def test_run_actions_runs_repo_list():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     expected = flexmock()
     expected = flexmock()
     flexmock(borgmatic.actions.repo_list).should_receive('run_repo_list').and_yield(expected).once()
     flexmock(borgmatic.actions.repo_list).should_receive('run_repo_list').and_yield(expected).once()
 
 
@@ -900,7 +938,7 @@ def test_run_actions_runs_repo_list():
 def test_run_actions_runs_list():
 def test_run_actions_runs_list():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     expected = flexmock()
     expected = flexmock()
     flexmock(borgmatic.actions.list).should_receive('run_list').and_yield(expected).once()
     flexmock(borgmatic.actions.list).should_receive('run_list').and_yield(expected).once()
 
 
@@ -922,7 +960,7 @@ def test_run_actions_runs_list():
 def test_run_actions_runs_repo_info():
 def test_run_actions_runs_repo_info():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     expected = flexmock()
     expected = flexmock()
     flexmock(borgmatic.actions.repo_info).should_receive('run_repo_info').and_yield(expected).once()
     flexmock(borgmatic.actions.repo_info).should_receive('run_repo_info').and_yield(expected).once()
 
 
@@ -944,7 +982,7 @@ def test_run_actions_runs_repo_info():
 def test_run_actions_runs_info():
 def test_run_actions_runs_info():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     expected = flexmock()
     expected = flexmock()
     flexmock(borgmatic.actions.info).should_receive('run_info').and_yield(expected).once()
     flexmock(borgmatic.actions.info).should_receive('run_info').and_yield(expected).once()
 
 
@@ -966,7 +1004,7 @@ def test_run_actions_runs_info():
 def test_run_actions_runs_break_lock():
 def test_run_actions_runs_break_lock():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.break_lock).should_receive('run_break_lock').once()
     flexmock(borgmatic.actions.break_lock).should_receive('run_break_lock').once()
 
 
     tuple(
     tuple(
@@ -986,7 +1024,7 @@ def test_run_actions_runs_break_lock():
 def test_run_actions_runs_export_key():
 def test_run_actions_runs_export_key():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.export_key).should_receive('run_export_key').once()
     flexmock(borgmatic.actions.export_key).should_receive('run_export_key').once()
 
 
     tuple(
     tuple(
@@ -1006,7 +1044,7 @@ def test_run_actions_runs_export_key():
 def test_run_actions_runs_change_passphrase():
 def test_run_actions_runs_change_passphrase():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.change_passphrase).should_receive('run_change_passphrase').once()
     flexmock(borgmatic.actions.change_passphrase).should_receive('run_change_passphrase').once()
 
 
     tuple(
     tuple(
@@ -1029,7 +1067,7 @@ def test_run_actions_runs_change_passphrase():
 def test_run_actions_runs_delete():
 def test_run_actions_runs_delete():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.delete).should_receive('run_delete').once()
     flexmock(borgmatic.actions.delete).should_receive('run_delete').once()
 
 
     tuple(
     tuple(
@@ -1049,7 +1087,7 @@ def test_run_actions_runs_delete():
 def test_run_actions_runs_repo_delete():
 def test_run_actions_runs_repo_delete():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.repo_delete).should_receive('run_repo_delete').once()
     flexmock(borgmatic.actions.repo_delete).should_receive('run_repo_delete').once()
 
 
     tuple(
     tuple(
@@ -1072,7 +1110,7 @@ def test_run_actions_runs_repo_delete():
 def test_run_actions_runs_borg():
 def test_run_actions_runs_borg():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.borg).should_receive('run_borg').once()
     flexmock(borgmatic.actions.borg).should_receive('run_borg').once()
 
 
     tuple(
     tuple(
@@ -1092,7 +1130,7 @@ def test_run_actions_runs_borg():
 def test_run_actions_runs_multiple_actions_in_argument_order():
 def test_run_actions_runs_multiple_actions_in_argument_order():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('get_skip_actions').and_return([])
     flexmock(module).should_receive('get_skip_actions').and_return([])
-    flexmock(module.command).should_receive('execute_hook')
+    flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
     flexmock(borgmatic.actions.borg).should_receive('run_borg').once().ordered()
     flexmock(borgmatic.actions.borg).should_receive('run_borg').once().ordered()
     flexmock(borgmatic.actions.restore).should_receive('run_restore').once().ordered()
     flexmock(borgmatic.actions.restore).should_receive('run_restore').once().ordered()
 
 
@@ -1398,11 +1436,12 @@ def test_collect_highlander_action_summary_logs_error_on_run_validate_failure():
 
 
 
 
 def test_collect_configuration_run_summary_logs_info_for_success():
 def test_collect_configuration_run_summary_logs_info_for_success():
-    flexmock(module.command).should_receive('execute_hook').never()
     flexmock(module.validate).should_receive('guard_configuration_contains_repository')
     flexmock(module.validate).should_receive('guard_configuration_contains_repository')
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_configuration').and_return([])
     flexmock(module).should_receive('run_configuration').and_return([])
-    arguments = {}
+    arguments = {'global': flexmock(dry_run=False)}
 
 
     logs = tuple(
     logs = tuple(
         module.collect_configuration_run_summary_logs(
         module.collect_configuration_run_summary_logs(
@@ -1415,6 +1454,8 @@ def test_collect_configuration_run_summary_logs_info_for_success():
 
 
 def test_collect_configuration_run_summary_executes_hooks_for_create():
 def test_collect_configuration_run_summary_executes_hooks_for_create():
     flexmock(module.validate).should_receive('guard_configuration_contains_repository')
     flexmock(module.validate).should_receive('guard_configuration_contains_repository')
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_configuration').and_return([])
     flexmock(module).should_receive('run_configuration').and_return([])
     arguments = {'create': flexmock(), 'global': flexmock(monitoring_verbosity=1, dry_run=False)}
     arguments = {'create': flexmock(), 'global': flexmock(monitoring_verbosity=1, dry_run=False)}
@@ -1430,9 +1471,11 @@ def test_collect_configuration_run_summary_executes_hooks_for_create():
 
 
 def test_collect_configuration_run_summary_logs_info_for_success_with_extract():
 def test_collect_configuration_run_summary_logs_info_for_success_with_extract():
     flexmock(module.validate).should_receive('guard_configuration_contains_repository')
     flexmock(module.validate).should_receive('guard_configuration_contains_repository')
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_configuration').and_return([])
     flexmock(module).should_receive('run_configuration').and_return([])
-    arguments = {'extract': flexmock(repository='repo')}
+    arguments = {'extract': flexmock(repository='repo'), 'global': flexmock(dry_run=False)}
 
 
     logs = tuple(
     logs = tuple(
         module.collect_configuration_run_summary_logs(
         module.collect_configuration_run_summary_logs(
@@ -1462,9 +1505,11 @@ def test_collect_configuration_run_summary_logs_extract_with_repository_error():
 
 
 def test_collect_configuration_run_summary_logs_info_for_success_with_mount():
 def test_collect_configuration_run_summary_logs_info_for_success_with_mount():
     flexmock(module.validate).should_receive('guard_configuration_contains_repository')
     flexmock(module.validate).should_receive('guard_configuration_contains_repository')
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_configuration').and_return([])
     flexmock(module).should_receive('run_configuration').and_return([])
-    arguments = {'mount': flexmock(repository='repo')}
+    arguments = {'mount': flexmock(repository='repo'), 'global': flexmock(dry_run=False)}
 
 
     logs = tuple(
     logs = tuple(
         module.collect_configuration_run_summary_logs(
         module.collect_configuration_run_summary_logs(
@@ -1481,7 +1526,7 @@ def test_collect_configuration_run_summary_logs_mount_with_repository_error():
     )
     )
     expected_logs = (flexmock(),)
     expected_logs = (flexmock(),)
     flexmock(module).should_receive('log_error_records').and_return(expected_logs)
     flexmock(module).should_receive('log_error_records').and_return(expected_logs)
-    arguments = {'mount': flexmock(repository='repo')}
+    arguments = {'mount': flexmock(repository='repo'), 'global': flexmock(dry_run=False)}
 
 
     logs = tuple(
     logs = tuple(
         module.collect_configuration_run_summary_logs(
         module.collect_configuration_run_summary_logs(
@@ -1493,6 +1538,9 @@ def test_collect_configuration_run_summary_logs_mount_with_repository_error():
 
 
 
 
 def test_collect_configuration_run_summary_logs_missing_configs_error():
 def test_collect_configuration_run_summary_logs_missing_configs_error():
+    flexmock(module.validate).should_receive('guard_configuration_contains_repository')
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     arguments = {'global': flexmock(config_paths=[])}
     arguments = {'global': flexmock(config_paths=[])}
     expected_logs = (flexmock(),)
     expected_logs = (flexmock(),)
     flexmock(module).should_receive('log_error_records').and_return(expected_logs)
     flexmock(module).should_receive('log_error_records').and_return(expected_logs)
@@ -1505,7 +1553,9 @@ def test_collect_configuration_run_summary_logs_missing_configs_error():
 
 
 
 
 def test_collect_configuration_run_summary_logs_pre_hook_error():
 def test_collect_configuration_run_summary_logs_pre_hook_error():
-    flexmock(module.command).should_receive('execute_hook').and_raise(ValueError)
+    flexmock(module.validate).should_receive('guard_configuration_contains_repository')
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks').and_raise(ValueError)
     expected_logs = (flexmock(),)
     expected_logs = (flexmock(),)
     flexmock(module).should_receive('log_error_records').and_return(expected_logs)
     flexmock(module).should_receive('log_error_records').and_return(expected_logs)
     arguments = {'create': flexmock(), 'global': flexmock(monitoring_verbosity=1, dry_run=False)}
     arguments = {'create': flexmock(), 'global': flexmock(monitoring_verbosity=1, dry_run=False)}
@@ -1520,8 +1570,9 @@ def test_collect_configuration_run_summary_logs_pre_hook_error():
 
 
 
 
 def test_collect_configuration_run_summary_logs_post_hook_error():
 def test_collect_configuration_run_summary_logs_post_hook_error():
-    flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(ValueError)
     flexmock(module.validate).should_receive('guard_configuration_contains_repository')
     flexmock(module.validate).should_receive('guard_configuration_contains_repository')
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks').and_return(None).and_raise(ValueError)
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_configuration').and_return([])
     flexmock(module).should_receive('run_configuration').and_return([])
     expected_logs = (flexmock(),)
     expected_logs = (flexmock(),)
@@ -1543,7 +1594,10 @@ def test_collect_configuration_run_summary_logs_for_list_with_archive_and_reposi
     )
     )
     expected_logs = (flexmock(),)
     expected_logs = (flexmock(),)
     flexmock(module).should_receive('log_error_records').and_return(expected_logs)
     flexmock(module).should_receive('log_error_records').and_return(expected_logs)
-    arguments = {'list': flexmock(repository='repo', archive='test')}
+    arguments = {
+        'list': flexmock(repository='repo', archive='test'),
+        'global': flexmock(dry_run=False),
+    }
 
 
     logs = tuple(
     logs = tuple(
         module.collect_configuration_run_summary_logs(
         module.collect_configuration_run_summary_logs(
@@ -1556,9 +1610,14 @@ def test_collect_configuration_run_summary_logs_for_list_with_archive_and_reposi
 
 
 def test_collect_configuration_run_summary_logs_info_for_success_with_list():
 def test_collect_configuration_run_summary_logs_info_for_success_with_list():
     flexmock(module.validate).should_receive('guard_configuration_contains_repository')
     flexmock(module.validate).should_receive('guard_configuration_contains_repository')
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_configuration').and_return([])
     flexmock(module).should_receive('run_configuration').and_return([])
-    arguments = {'list': flexmock(repository='repo', archive=None)}
+    arguments = {
+        'list': flexmock(repository='repo', archive=None),
+        'global': flexmock(dry_run=False),
+    }
 
 
     logs = tuple(
     logs = tuple(
         module.collect_configuration_run_summary_logs(
         module.collect_configuration_run_summary_logs(
@@ -1571,12 +1630,14 @@ def test_collect_configuration_run_summary_logs_info_for_success_with_list():
 
 
 def test_collect_configuration_run_summary_logs_run_configuration_error():
 def test_collect_configuration_run_summary_logs_run_configuration_error():
     flexmock(module.validate).should_receive('guard_configuration_contains_repository')
     flexmock(module.validate).should_receive('guard_configuration_contains_repository')
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_configuration').and_return(
     flexmock(module).should_receive('run_configuration').and_return(
         [logging.makeLogRecord(dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg='Error'))]
         [logging.makeLogRecord(dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg='Error'))]
     )
     )
     flexmock(module).should_receive('log_error_records').and_return([])
     flexmock(module).should_receive('log_error_records').and_return([])
-    arguments = {}
+    arguments = {'global': flexmock(dry_run=False)}
 
 
     logs = tuple(
     logs = tuple(
         module.collect_configuration_run_summary_logs(
         module.collect_configuration_run_summary_logs(
@@ -1589,13 +1650,15 @@ def test_collect_configuration_run_summary_logs_run_configuration_error():
 
 
 def test_collect_configuration_run_summary_logs_run_umount_error():
 def test_collect_configuration_run_summary_logs_run_umount_error():
     flexmock(module.validate).should_receive('guard_configuration_contains_repository')
     flexmock(module.validate).should_receive('guard_configuration_contains_repository')
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_configuration').and_return([])
     flexmock(module).should_receive('run_configuration').and_return([])
     flexmock(module.borg_umount).should_receive('unmount_archive').and_raise(OSError)
     flexmock(module.borg_umount).should_receive('unmount_archive').and_raise(OSError)
     flexmock(module).should_receive('log_error_records').and_return(
     flexmock(module).should_receive('log_error_records').and_return(
         [logging.makeLogRecord(dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg='Error'))]
         [logging.makeLogRecord(dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg='Error'))]
     )
     )
-    arguments = {'umount': flexmock(mount_point='/mnt')}
+    arguments = {'umount': flexmock(mount_point='/mnt'), 'global': flexmock(dry_run=False)}
 
 
     logs = tuple(
     logs = tuple(
         module.collect_configuration_run_summary_logs(
         module.collect_configuration_run_summary_logs(
@@ -1608,6 +1671,8 @@ def test_collect_configuration_run_summary_logs_run_umount_error():
 
 
 def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
 def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
     flexmock(module.validate).should_receive('guard_configuration_contains_repository')
     flexmock(module.validate).should_receive('guard_configuration_contains_repository')
+    flexmock(module.command).should_receive('filter_hooks')
+    flexmock(module.command).should_receive('execute_hooks')
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('Log_prefix').and_return(flexmock())
     flexmock(module).should_receive('run_configuration').and_return(['foo', 'bar']).and_return(
     flexmock(module).should_receive('run_configuration').and_return(['foo', 'bar']).and_return(
         ['baz']
         ['baz']
@@ -1615,7 +1680,7 @@ def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
     stdout = flexmock()
     stdout = flexmock()
     stdout.should_receive('write').with_args('["foo", "bar", "baz"]').once()
     stdout.should_receive('write').with_args('["foo", "bar", "baz"]').once()
     flexmock(module.sys).stdout = stdout
     flexmock(module.sys).stdout = stdout
-    arguments = {}
+    arguments = {'global': flexmock(dry_run=False)}
 
 
     tuple(
     tuple(
         module.collect_configuration_run_summary_logs(
         module.collect_configuration_run_summary_logs(

+ 0 - 4
tests/unit/config/test_generate.py

@@ -133,7 +133,6 @@ def test_schema_to_sample_configuration_with_unsupported_schema_raises():
 def test_merge_source_configuration_into_destination_inserts_map_fields():
 def test_merge_source_configuration_into_destination_inserts_map_fields():
     destination_config = {'foo': 'dest1', 'bar': 'dest2'}
     destination_config = {'foo': 'dest1', 'bar': 'dest2'}
     source_config = {'foo': 'source1', 'baz': 'source2'}
     source_config = {'foo': 'source1', 'baz': 'source2'}
-    flexmock(module).should_receive('remove_commented_out_sentinel')
     flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
     flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
 
 
     module.merge_source_configuration_into_destination(destination_config, source_config)
     module.merge_source_configuration_into_destination(destination_config, source_config)
@@ -144,7 +143,6 @@ def test_merge_source_configuration_into_destination_inserts_map_fields():
 def test_merge_source_configuration_into_destination_inserts_nested_map_fields():
 def test_merge_source_configuration_into_destination_inserts_nested_map_fields():
     destination_config = {'foo': {'first': 'dest1', 'second': 'dest2'}, 'bar': 'dest3'}
     destination_config = {'foo': {'first': 'dest1', 'second': 'dest2'}, 'bar': 'dest3'}
     source_config = {'foo': {'first': 'source1'}}
     source_config = {'foo': {'first': 'source1'}}
-    flexmock(module).should_receive('remove_commented_out_sentinel')
     flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
     flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
 
 
     module.merge_source_configuration_into_destination(destination_config, source_config)
     module.merge_source_configuration_into_destination(destination_config, source_config)
@@ -155,7 +153,6 @@ def test_merge_source_configuration_into_destination_inserts_nested_map_fields()
 def test_merge_source_configuration_into_destination_inserts_sequence_fields():
 def test_merge_source_configuration_into_destination_inserts_sequence_fields():
     destination_config = {'foo': ['dest1', 'dest2'], 'bar': ['dest3'], 'baz': ['dest4']}
     destination_config = {'foo': ['dest1', 'dest2'], 'bar': ['dest3'], 'baz': ['dest4']}
     source_config = {'foo': ['source1'], 'bar': ['source2', 'source3']}
     source_config = {'foo': ['source1'], 'bar': ['source2', 'source3']}
-    flexmock(module).should_receive('remove_commented_out_sentinel')
     flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
     flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
 
 
     module.merge_source_configuration_into_destination(destination_config, source_config)
     module.merge_source_configuration_into_destination(destination_config, source_config)
@@ -170,7 +167,6 @@ def test_merge_source_configuration_into_destination_inserts_sequence_fields():
 def test_merge_source_configuration_into_destination_inserts_sequence_of_maps():
 def test_merge_source_configuration_into_destination_inserts_sequence_of_maps():
     destination_config = {'foo': [{'first': 'dest1', 'second': 'dest2'}], 'bar': 'dest3'}
     destination_config = {'foo': [{'first': 'dest1', 'second': 'dest2'}], 'bar': 'dest3'}
     source_config = {'foo': [{'first': 'source1'}, {'other': 'source2'}]}
     source_config = {'foo': [{'first': 'source1'}, {'other': 'source2'}]}
-    flexmock(module).should_receive('remove_commented_out_sentinel')
     flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
     flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
 
 
     module.merge_source_configuration_into_destination(destination_config, source_config)
     module.merge_source_configuration_into_destination(destination_config, source_config)

+ 7 - 0
tests/unit/hooks/data_source/test_bootstrap.py

@@ -6,6 +6,9 @@ from borgmatic.hooks.data_source import bootstrap as module
 
 
 
 
 def test_dump_data_sources_creates_manifest_file():
 def test_dump_data_sources_creates_manifest_file():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     flexmock(module.os).should_receive('makedirs')
     flexmock(module.os).should_receive('makedirs')
 
 
     flexmock(module.importlib.metadata).should_receive('version').and_return('1.0.0')
     flexmock(module.importlib.metadata).should_receive('version').and_return('1.0.0')
@@ -32,6 +35,7 @@ def test_dump_data_sources_creates_manifest_file():
 
 
 
 
 def test_dump_data_sources_with_store_config_files_false_does_not_create_manifest_file():
 def test_dump_data_sources_with_store_config_files_false_does_not_create_manifest_file():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').never()
     flexmock(module.os).should_receive('makedirs').never()
     flexmock(module.os).should_receive('makedirs').never()
     flexmock(module.json).should_receive('dump').never()
     flexmock(module.json).should_receive('dump').never()
     hook_config = {'store_config_files': False}
     hook_config = {'store_config_files': False}
@@ -47,6 +51,9 @@ def test_dump_data_sources_with_store_config_files_false_does_not_create_manifes
 
 
 
 
 def test_dump_data_sources_with_dry_run_does_not_create_manifest_file():
 def test_dump_data_sources_with_dry_run_does_not_create_manifest_file():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     flexmock(module.os).should_receive('makedirs').never()
     flexmock(module.os).should_receive('makedirs').never()
     flexmock(module.json).should_receive('dump').never()
     flexmock(module.json).should_receive('dump').never()
 
 

+ 18 - 0
tests/unit/hooks/data_source/test_btrfs.py

@@ -207,6 +207,9 @@ def test_make_borg_snapshot_pattern_includes_slashdot_hack_and_stripped_pattern_
 
 
 
 
 def test_dump_data_sources_snapshots_each_subvolume_and_updates_patterns():
 def test_dump_data_sources_snapshots_each_subvolume_and_updates_patterns():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
     patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
     config = {'btrfs': {}}
     config = {'btrfs': {}}
     flexmock(module).should_receive('get_subvolumes').and_return(
     flexmock(module).should_receive('get_subvolumes').and_return(
@@ -285,6 +288,9 @@ def test_dump_data_sources_snapshots_each_subvolume_and_updates_patterns():
 
 
 
 
 def test_dump_data_sources_uses_custom_btrfs_command_in_commands():
 def test_dump_data_sources_uses_custom_btrfs_command_in_commands():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
     patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
     config = {'btrfs': {'btrfs_command': '/usr/local/bin/btrfs'}}
     config = {'btrfs': {'btrfs_command': '/usr/local/bin/btrfs'}}
     flexmock(module).should_receive('get_subvolumes').and_return(
     flexmock(module).should_receive('get_subvolumes').and_return(
@@ -338,6 +344,9 @@ def test_dump_data_sources_uses_custom_btrfs_command_in_commands():
 
 
 
 
 def test_dump_data_sources_uses_custom_findmnt_command_in_commands():
 def test_dump_data_sources_uses_custom_findmnt_command_in_commands():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
     patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
     config = {'btrfs': {'findmnt_command': '/usr/local/bin/findmnt'}}
     config = {'btrfs': {'findmnt_command': '/usr/local/bin/findmnt'}}
     flexmock(module).should_receive('get_subvolumes').with_args(
     flexmock(module).should_receive('get_subvolumes').with_args(
@@ -393,6 +402,9 @@ def test_dump_data_sources_uses_custom_findmnt_command_in_commands():
 
 
 
 
 def test_dump_data_sources_with_dry_run_skips_snapshot_and_patterns_update():
 def test_dump_data_sources_with_dry_run_skips_snapshot_and_patterns_update():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
     patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
     config = {'btrfs': {}}
     config = {'btrfs': {}}
     flexmock(module).should_receive('get_subvolumes').and_return(
     flexmock(module).should_receive('get_subvolumes').and_return(
@@ -421,6 +433,9 @@ def test_dump_data_sources_with_dry_run_skips_snapshot_and_patterns_update():
 
 
 
 
 def test_dump_data_sources_without_matching_subvolumes_skips_snapshot_and_patterns_update():
 def test_dump_data_sources_without_matching_subvolumes_skips_snapshot_and_patterns_update():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
     patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
     config = {'btrfs': {}}
     config = {'btrfs': {}}
     flexmock(module).should_receive('get_subvolumes').and_return(())
     flexmock(module).should_receive('get_subvolumes').and_return(())
@@ -445,6 +460,9 @@ def test_dump_data_sources_without_matching_subvolumes_skips_snapshot_and_patter
 
 
 
 
 def test_dump_data_sources_snapshots_adds_to_existing_exclude_patterns():
 def test_dump_data_sources_snapshots_adds_to_existing_exclude_patterns():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
     patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
     config = {'btrfs': {}, 'exclude_patterns': ['/bar']}
     config = {'btrfs': {}, 'exclude_patterns': ['/bar']}
     flexmock(module).should_receive('get_subvolumes').and_return(
     flexmock(module).should_receive('get_subvolumes').and_return(

+ 21 - 0
tests/unit/hooks/data_source/test_lvm.py

@@ -282,6 +282,9 @@ def test_make_borg_snapshot_pattern_includes_slashdot_hack_and_stripped_pattern_
 
 
 
 
 def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
 def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     config = {'lvm': {}}
     config = {'lvm': {}}
     patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
     patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
     logical_volumes = (
     logical_volumes = (
@@ -351,6 +354,9 @@ def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
 
 
 
 
 def test_dump_data_sources_with_no_logical_volumes_skips_snapshots():
 def test_dump_data_sources_with_no_logical_volumes_skips_snapshots():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     config = {'lvm': {}}
     config = {'lvm': {}}
     patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
     patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
     flexmock(module).should_receive('get_logical_volumes').and_return(())
     flexmock(module).should_receive('get_logical_volumes').and_return(())
@@ -373,6 +379,9 @@ def test_dump_data_sources_with_no_logical_volumes_skips_snapshots():
 
 
 
 
 def test_dump_data_sources_uses_snapshot_size_for_snapshot():
 def test_dump_data_sources_uses_snapshot_size_for_snapshot():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     config = {'lvm': {'snapshot_size': '1000PB'}}
     config = {'lvm': {'snapshot_size': '1000PB'}}
     patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
     patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
     logical_volumes = (
     logical_volumes = (
@@ -448,6 +457,9 @@ def test_dump_data_sources_uses_snapshot_size_for_snapshot():
 
 
 
 
 def test_dump_data_sources_uses_custom_commands():
 def test_dump_data_sources_uses_custom_commands():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     config = {
     config = {
         'lvm': {
         'lvm': {
             'lsblk_command': '/usr/local/bin/lsblk',
             'lsblk_command': '/usr/local/bin/lsblk',
@@ -534,6 +546,9 @@ def test_dump_data_sources_uses_custom_commands():
 
 
 
 
 def test_dump_data_sources_with_dry_run_skips_snapshots_and_does_not_touch_patterns():
 def test_dump_data_sources_with_dry_run_skips_snapshots_and_does_not_touch_patterns():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     config = {'lvm': {}}
     config = {'lvm': {}}
     patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
     patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
     flexmock(module).should_receive('get_logical_volumes').and_return(
     flexmock(module).should_receive('get_logical_volumes').and_return(
@@ -585,6 +600,9 @@ def test_dump_data_sources_with_dry_run_skips_snapshots_and_does_not_touch_patte
 
 
 
 
 def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained_patterns():
 def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained_patterns():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     config = {'lvm': {}}
     config = {'lvm': {}}
     patterns = [Pattern('/hmm')]
     patterns = [Pattern('/hmm')]
     logical_volumes = (
     logical_volumes = (
@@ -655,6 +673,9 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained
 
 
 
 
 def test_dump_data_sources_with_missing_snapshot_errors():
 def test_dump_data_sources_with_missing_snapshot_errors():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     config = {'lvm': {}}
     config = {'lvm': {}}
     patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
     patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
     flexmock(module).should_receive('get_logical_volumes').and_return(
     flexmock(module).should_receive('get_logical_volumes').and_return(

+ 18 - 0
tests/unit/hooks/data_source/test_mariadb.py

@@ -237,6 +237,9 @@ def test_use_streaming_false_for_no_databases():
 
 
 
 
 def test_dump_data_sources_dumps_each_database():
 def test_dump_data_sources_dumps_each_database():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     processes = [flexmock(), flexmock()]
     processes = [flexmock(), flexmock()]
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -278,6 +281,9 @@ def test_dump_data_sources_dumps_each_database():
 
 
 
 
 def test_dump_data_sources_dumps_with_password():
 def test_dump_data_sources_dumps_with_password():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     database = {'name': 'foo', 'username': 'root', 'password': 'trustsome1'}
     database = {'name': 'foo', 'username': 'root', 'password': 'trustsome1'}
     process = flexmock()
     process = flexmock()
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -312,6 +318,9 @@ def test_dump_data_sources_dumps_with_password():
 
 
 
 
 def test_dump_data_sources_dumps_all_databases_at_once():
 def test_dump_data_sources_dumps_all_databases_at_once():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all'}]
     databases = [{'name': 'all'}]
     process = flexmock()
     process = flexmock()
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -343,6 +352,9 @@ def test_dump_data_sources_dumps_all_databases_at_once():
 
 
 
 
 def test_dump_data_sources_dumps_all_databases_separately_when_format_configured():
 def test_dump_data_sources_dumps_all_databases_separately_when_format_configured():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all', 'format': 'sql'}]
     databases = [{'name': 'all', 'format': 'sql'}]
     processes = [flexmock(), flexmock()]
     processes = [flexmock(), flexmock()]
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -850,6 +862,9 @@ def test_execute_dump_command_with_dry_run_skips_mariadb_dump():
 
 
 
 
 def test_dump_data_sources_errors_for_missing_all_databases():
 def test_dump_data_sources_errors_for_missing_all_databases():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all'}]
     databases = [{'name': 'all'}]
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
     flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
@@ -873,6 +888,9 @@ def test_dump_data_sources_errors_for_missing_all_databases():
 
 
 
 
 def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
 def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all'}]
     databases = [{'name': 'all'}]
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
     flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})

+ 21 - 0
tests/unit/hooks/data_source/test_mongodb.py

@@ -24,6 +24,9 @@ def test_use_streaming_false_for_no_databases():
 
 
 
 
 def test_dump_data_sources_runs_mongodump_for_each_database():
 def test_dump_data_sources_runs_mongodump_for_each_database():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     processes = [flexmock(), flexmock()]
     processes = [flexmock(), flexmock()]
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -53,6 +56,9 @@ def test_dump_data_sources_runs_mongodump_for_each_database():
 
 
 
 
 def test_dump_data_sources_with_dry_run_skips_mongodump():
 def test_dump_data_sources_with_dry_run_skips_mongodump():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
     flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
@@ -75,6 +81,9 @@ def test_dump_data_sources_with_dry_run_skips_mongodump():
 
 
 
 
 def test_dump_data_sources_runs_mongodump_with_hostname_and_port():
 def test_dump_data_sources_runs_mongodump_with_hostname_and_port():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
     databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
     process = flexmock()
     process = flexmock()
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -111,6 +120,9 @@ def test_dump_data_sources_runs_mongodump_with_hostname_and_port():
 
 
 
 
 def test_dump_data_sources_runs_mongodump_with_username_and_password():
 def test_dump_data_sources_runs_mongodump_with_username_and_password():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [
     databases = [
         {
         {
             'name': 'foo',
             'name': 'foo',
@@ -162,6 +174,9 @@ def test_dump_data_sources_runs_mongodump_with_username_and_password():
 
 
 
 
 def test_dump_data_sources_runs_mongodump_with_directory_format():
 def test_dump_data_sources_runs_mongodump_with_directory_format():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'format': 'directory'}]
     databases = [{'name': 'foo', 'format': 'directory'}]
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
     flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
@@ -189,6 +204,9 @@ def test_dump_data_sources_runs_mongodump_with_directory_format():
 
 
 
 
 def test_dump_data_sources_runs_mongodump_with_options():
 def test_dump_data_sources_runs_mongodump_with_options():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'options': '--stuff=such'}]
     databases = [{'name': 'foo', 'options': '--stuff=such'}]
     process = flexmock()
     process = flexmock()
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -222,6 +240,9 @@ def test_dump_data_sources_runs_mongodump_with_options():
 
 
 
 
 def test_dump_data_sources_runs_mongodumpall_for_all_databases():
 def test_dump_data_sources_runs_mongodumpall_for_all_databases():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all'}]
     databases = [{'name': 'all'}]
     process = flexmock()
     process = flexmock()
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')

+ 18 - 0
tests/unit/hooks/data_source/test_mysql.py

@@ -134,6 +134,9 @@ def test_use_streaming_false_for_no_databases():
 
 
 
 
 def test_dump_data_sources_dumps_each_database():
 def test_dump_data_sources_dumps_each_database():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     processes = [flexmock(), flexmock()]
     processes = [flexmock(), flexmock()]
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -172,6 +175,9 @@ def test_dump_data_sources_dumps_each_database():
 
 
 
 
 def test_dump_data_sources_dumps_with_password():
 def test_dump_data_sources_dumps_with_password():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     database = {'name': 'foo', 'username': 'root', 'password': 'trustsome1'}
     database = {'name': 'foo', 'username': 'root', 'password': 'trustsome1'}
     process = flexmock()
     process = flexmock()
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -206,6 +212,9 @@ def test_dump_data_sources_dumps_with_password():
 
 
 
 
 def test_dump_data_sources_dumps_all_databases_at_once():
 def test_dump_data_sources_dumps_all_databases_at_once():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all'}]
     databases = [{'name': 'all'}]
     process = flexmock()
     process = flexmock()
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -237,6 +246,9 @@ def test_dump_data_sources_dumps_all_databases_at_once():
 
 
 
 
 def test_dump_data_sources_dumps_all_databases_separately_when_format_configured():
 def test_dump_data_sources_dumps_all_databases_separately_when_format_configured():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all', 'format': 'sql'}]
     databases = [{'name': 'all', 'format': 'sql'}]
     processes = [flexmock(), flexmock()]
     processes = [flexmock(), flexmock()]
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -762,6 +774,9 @@ def test_execute_dump_command_with_dry_run_skips_mysqldump():
 
 
 
 
 def test_dump_data_sources_errors_for_missing_all_databases():
 def test_dump_data_sources_errors_for_missing_all_databases():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all'}]
     databases = [{'name': 'all'}]
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
     flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
@@ -785,6 +800,9 @@ def test_dump_data_sources_errors_for_missing_all_databases():
 
 
 
 
 def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
 def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all'}]
     databases = [{'name': 'all'}]
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
     flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})

+ 42 - 0
tests/unit/hooks/data_source/test_postgresql.py

@@ -236,6 +236,9 @@ def test_use_streaming_false_for_no_databases():
 
 
 
 
 def test_dump_data_sources_runs_pg_dump_for_each_database():
 def test_dump_data_sources_runs_pg_dump_for_each_database():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     processes = [flexmock(), flexmock()]
     processes = [flexmock(), flexmock()]
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
@@ -284,6 +287,9 @@ def test_dump_data_sources_runs_pg_dump_for_each_database():
 
 
 
 
 def test_dump_data_sources_raises_when_no_database_names_to_dump():
 def test_dump_data_sources_raises_when_no_database_names_to_dump():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -301,6 +307,9 @@ def test_dump_data_sources_raises_when_no_database_names_to_dump():
 
 
 
 
 def test_dump_data_sources_does_not_raise_when_no_database_names_to_dump():
 def test_dump_data_sources_does_not_raise_when_no_database_names_to_dump():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -317,6 +326,9 @@ def test_dump_data_sources_does_not_raise_when_no_database_names_to_dump():
 
 
 
 
 def test_dump_data_sources_with_duplicate_dump_skips_pg_dump():
 def test_dump_data_sources_with_duplicate_dump_skips_pg_dump():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -344,6 +356,9 @@ def test_dump_data_sources_with_duplicate_dump_skips_pg_dump():
 
 
 
 
 def test_dump_data_sources_with_dry_run_skips_pg_dump():
 def test_dump_data_sources_with_dry_run_skips_pg_dump():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     databases = [{'name': 'foo'}, {'name': 'bar'}]
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -374,6 +389,9 @@ def test_dump_data_sources_with_dry_run_skips_pg_dump():
 
 
 
 
 def test_dump_data_sources_runs_pg_dump_with_hostname_and_port():
 def test_dump_data_sources_runs_pg_dump_with_hostname_and_port():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
     databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
     process = flexmock()
     process = flexmock()
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
@@ -420,6 +438,9 @@ def test_dump_data_sources_runs_pg_dump_with_hostname_and_port():
 
 
 
 
 def test_dump_data_sources_runs_pg_dump_with_username_and_password():
 def test_dump_data_sources_runs_pg_dump_with_username_and_password():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'username': 'postgres', 'password': 'trustsome1'}]
     databases = [{'name': 'foo', 'username': 'postgres', 'password': 'trustsome1'}]
     process = flexmock()
     process = flexmock()
     flexmock(module).should_receive('make_environment').and_return(
     flexmock(module).should_receive('make_environment').and_return(
@@ -466,6 +487,9 @@ def test_dump_data_sources_runs_pg_dump_with_username_and_password():
 
 
 
 
 def test_dump_data_sources_with_username_injection_attack_gets_escaped():
 def test_dump_data_sources_with_username_injection_attack_gets_escaped():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'username': 'postgres; naughty-command', 'password': 'trustsome1'}]
     databases = [{'name': 'foo', 'username': 'postgres; naughty-command', 'password': 'trustsome1'}]
     process = flexmock()
     process = flexmock()
     flexmock(module).should_receive('make_environment').and_return(
     flexmock(module).should_receive('make_environment').and_return(
@@ -512,6 +536,9 @@ def test_dump_data_sources_with_username_injection_attack_gets_escaped():
 
 
 
 
 def test_dump_data_sources_runs_pg_dump_with_directory_format():
 def test_dump_data_sources_runs_pg_dump_with_directory_format():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'format': 'directory'}]
     databases = [{'name': 'foo', 'format': 'directory'}]
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
@@ -556,6 +583,9 @@ def test_dump_data_sources_runs_pg_dump_with_directory_format():
 
 
 
 
 def test_dump_data_sources_runs_pg_dump_with_string_compression():
 def test_dump_data_sources_runs_pg_dump_with_string_compression():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'compression': 'winrar'}]
     databases = [{'name': 'foo', 'compression': 'winrar'}]
     processes = [flexmock()]
     processes = [flexmock()]
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
@@ -603,6 +633,9 @@ def test_dump_data_sources_runs_pg_dump_with_string_compression():
 
 
 
 
 def test_dump_data_sources_runs_pg_dump_with_integer_compression():
 def test_dump_data_sources_runs_pg_dump_with_integer_compression():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'compression': 0}]
     databases = [{'name': 'foo', 'compression': 0}]
     processes = [flexmock()]
     processes = [flexmock()]
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
@@ -650,6 +683,9 @@ def test_dump_data_sources_runs_pg_dump_with_integer_compression():
 
 
 
 
 def test_dump_data_sources_runs_pg_dump_with_options():
 def test_dump_data_sources_runs_pg_dump_with_options():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'options': '--stuff=such'}]
     databases = [{'name': 'foo', 'options': '--stuff=such'}]
     process = flexmock()
     process = flexmock()
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
@@ -693,6 +729,9 @@ def test_dump_data_sources_runs_pg_dump_with_options():
 
 
 
 
 def test_dump_data_sources_runs_pg_dumpall_for_all_databases():
 def test_dump_data_sources_runs_pg_dumpall_for_all_databases():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'all'}]
     databases = [{'name': 'all'}]
     process = flexmock()
     process = flexmock()
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
@@ -725,6 +764,9 @@ def test_dump_data_sources_runs_pg_dumpall_for_all_databases():
 
 
 
 
 def test_dump_data_sources_runs_non_default_pg_dump():
 def test_dump_data_sources_runs_non_default_pg_dump():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'name': 'foo', 'pg_dump_command': 'special_pg_dump --compress *'}]
     databases = [{'name': 'foo', 'pg_dump_command': 'special_pg_dump --compress *'}]
     process = flexmock()
     process = flexmock()
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})

+ 18 - 0
tests/unit/hooks/data_source/test_sqlite.py

@@ -17,6 +17,9 @@ def test_use_streaming_false_for_no_databases():
 
 
 
 
 def test_dump_data_sources_logs_and_skips_if_dump_already_exists():
 def test_dump_data_sources_logs_and_skips_if_dump_already_exists():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'path': '/path/to/database', 'name': 'database'}]
     databases = [{'path': '/path/to/database', 'name': 'database'}]
 
 
     flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
     flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
@@ -41,6 +44,9 @@ def test_dump_data_sources_logs_and_skips_if_dump_already_exists():
 
 
 
 
 def test_dump_data_sources_dumps_each_database():
 def test_dump_data_sources_dumps_each_database():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [
     databases = [
         {'path': '/path/to/database1', 'name': 'database1'},
         {'path': '/path/to/database1', 'name': 'database1'},
         {'path': '/path/to/database2', 'name': 'database2'},
         {'path': '/path/to/database2', 'name': 'database2'},
@@ -71,6 +77,9 @@ def test_dump_data_sources_dumps_each_database():
 
 
 
 
 def test_dump_data_sources_with_path_injection_attack_gets_escaped():
 def test_dump_data_sources_with_path_injection_attack_gets_escaped():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [
     databases = [
         {'path': '/path/to/database1; naughty-command', 'name': 'database1'},
         {'path': '/path/to/database1; naughty-command', 'name': 'database1'},
     ]
     ]
@@ -108,6 +117,9 @@ def test_dump_data_sources_with_path_injection_attack_gets_escaped():
 
 
 
 
 def test_dump_data_sources_with_non_existent_path_warns_and_dumps_database():
 def test_dump_data_sources_with_non_existent_path_warns_and_dumps_database():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [
     databases = [
         {'path': '/path/to/database1', 'name': 'database1'},
         {'path': '/path/to/database1', 'name': 'database1'},
     ]
     ]
@@ -136,6 +148,9 @@ def test_dump_data_sources_with_non_existent_path_warns_and_dumps_database():
 
 
 
 
 def test_dump_data_sources_with_name_all_warns_and_dumps_all_databases():
 def test_dump_data_sources_with_name_all_warns_and_dumps_all_databases():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [
     databases = [
         {'path': '/path/to/database1', 'name': 'all'},
         {'path': '/path/to/database1', 'name': 'all'},
     ]
     ]
@@ -166,6 +181,9 @@ def test_dump_data_sources_with_name_all_warns_and_dumps_all_databases():
 
 
 
 
 def test_dump_data_sources_does_not_dump_if_dry_run():
 def test_dump_data_sources_does_not_dump_if_dry_run():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     databases = [{'path': '/path/to/database', 'name': 'database'}]
     databases = [{'path': '/path/to/database', 'name': 'database'}]
 
 
     flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
     flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')

+ 15 - 0
tests/unit/hooks/data_source/test_zfs.py

@@ -296,6 +296,9 @@ def test_make_borg_snapshot_pattern_includes_slashdot_hack_and_stripped_pattern_
 
 
 
 
 def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
 def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     dataset = flexmock(
     dataset = flexmock(
         name='dataset',
         name='dataset',
         mount_point='/mnt/dataset',
         mount_point='/mnt/dataset',
@@ -338,6 +341,9 @@ def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
 
 
 
 
 def test_dump_data_sources_with_no_datasets_skips_snapshots():
 def test_dump_data_sources_with_no_datasets_skips_snapshots():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     flexmock(module).should_receive('get_datasets_to_backup').and_return(())
     flexmock(module).should_receive('get_datasets_to_backup').and_return(())
     flexmock(module.os).should_receive('getpid').and_return(1234)
     flexmock(module.os).should_receive('getpid').and_return(1234)
     flexmock(module).should_receive('snapshot_dataset').never()
     flexmock(module).should_receive('snapshot_dataset').never()
@@ -360,6 +366,9 @@ def test_dump_data_sources_with_no_datasets_skips_snapshots():
 
 
 
 
 def test_dump_data_sources_uses_custom_commands():
 def test_dump_data_sources_uses_custom_commands():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     dataset = flexmock(
     dataset = flexmock(
         name='dataset',
         name='dataset',
         mount_point='/mnt/dataset',
         mount_point='/mnt/dataset',
@@ -409,6 +418,9 @@ def test_dump_data_sources_uses_custom_commands():
 
 
 
 
 def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_patterns():
 def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_patterns():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     flexmock(module).should_receive('get_datasets_to_backup').and_return(
     flexmock(module).should_receive('get_datasets_to_backup').and_return(
         (flexmock(name='dataset', mount_point='/mnt/dataset'),)
         (flexmock(name='dataset', mount_point='/mnt/dataset'),)
     )
     )
@@ -433,6 +445,9 @@ def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_patter
 
 
 
 
 def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained_patterns():
 def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained_patterns():
+    flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
+        flexmock()
+    )
     dataset = flexmock(
     dataset = flexmock(
         name='dataset',
         name='dataset',
         mount_point='/mnt/dataset',
         mount_point='/mnt/dataset',

+ 34 - 38
tests/unit/hooks/test_command.py

@@ -48,43 +48,35 @@ def test_make_environment_with_pyinstaller_and_LD_LIBRARY_PATH_ORIG_copies_it_in
     ) == {'LD_LIBRARY_PATH_ORIG': '/lib/lib/lib', 'LD_LIBRARY_PATH': '/lib/lib/lib'}
     ) == {'LD_LIBRARY_PATH_ORIG': '/lib/lib/lib', 'LD_LIBRARY_PATH': '/lib/lib/lib'}
 
 
 
 
-def test_execute_hook_invokes_each_command():
-    flexmock(module).should_receive('interpolate_context').replace_with(
-        lambda hook_description, command, context: command
-    )
-    flexmock(module).should_receive('make_environment').and_return({})
-    flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
-        [':'],
-        output_log_level=logging.WARNING,
-        shell=True,
-        environment={},
-    ).once()
-
-    module.execute_hook([':'], None, 'config.yaml', 'pre-backup', dry_run=False)
+LOGGING_ANSWER = flexmock()
 
 
 
 
-def test_execute_hook_with_multiple_commands_invokes_each_command():
+def test_execute_hooks_invokes_each_hook_and_command():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = LOGGING_ANSWER
     flexmock(module).should_receive('interpolate_context').replace_with(
     flexmock(module).should_receive('interpolate_context').replace_with(
         lambda hook_description, command, context: command
         lambda hook_description, command, context: command
     )
     )
     flexmock(module).should_receive('make_environment').and_return({})
     flexmock(module).should_receive('make_environment').and_return({})
-    flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
-        [':'],
-        output_log_level=logging.WARNING,
-        shell=True,
-        environment={},
-    ).once()
-    flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
-        ['true'],
-        output_log_level=logging.WARNING,
-        shell=True,
-        environment={},
-    ).once()
 
 
-    module.execute_hook([':', 'true'], None, 'config.yaml', 'pre-backup', dry_run=False)
+    for command in ('foo', 'bar', 'baz'):
+        flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
+            [command],
+            output_log_level=LOGGING_ANSWER,
+            shell=True,
+            environment={},
+        ).once()
+
+    module.execute_hooks(
+        [{'before': 'create', 'run': ['foo']}, {'before': 'create', 'run': ['bar', 'baz']}],
+        umask=None,
+        dry_run=False,
+    )
 
 
 
 
-def test_execute_hook_with_umask_sets_that_umask():
+def test_execute_hooks_with_umask_sets_that_umask():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = LOGGING_ANSWER
     flexmock(module).should_receive('interpolate_context').replace_with(
     flexmock(module).should_receive('interpolate_context').replace_with(
         lambda hook_description, command, context: command
         lambda hook_description, command, context: command
     )
     )
@@ -92,42 +84,46 @@ def test_execute_hook_with_umask_sets_that_umask():
     flexmock(module.os).should_receive('umask').with_args(0o22).once()
     flexmock(module.os).should_receive('umask').with_args(0o22).once()
     flexmock(module).should_receive('make_environment').and_return({})
     flexmock(module).should_receive('make_environment').and_return({})
     flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
     flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
-        [':'],
-        output_log_level=logging.WARNING,
+        ['foo'],
+        output_log_level=logging.ANSWER,
         shell=True,
         shell=True,
         environment={},
         environment={},
     )
     )
 
 
-    module.execute_hook([':'], 77, 'config.yaml', 'pre-backup', dry_run=False)
+    module.execute_hooks([{'before': 'create', 'run': ['foo']}], umask=77, dry_run=False)
 
 
 
 
-def test_execute_hook_with_dry_run_skips_commands():
+def test_execute_hooks_with_dry_run_skips_commands():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = LOGGING_ANSWER
     flexmock(module).should_receive('interpolate_context').replace_with(
     flexmock(module).should_receive('interpolate_context').replace_with(
         lambda hook_description, command, context: command
         lambda hook_description, command, context: command
     )
     )
     flexmock(module).should_receive('make_environment').and_return({})
     flexmock(module).should_receive('make_environment').and_return({})
     flexmock(module.borgmatic.execute).should_receive('execute_command').never()
     flexmock(module.borgmatic.execute).should_receive('execute_command').never()
 
 
-    module.execute_hook([':', 'true'], None, 'config.yaml', 'pre-backup', dry_run=True)
+    module.execute_hooks([{'before': 'create', 'run': ['foo']}], umask=None, dry_run=True)
 
 
 
 
-def test_execute_hook_with_empty_commands_does_not_raise():
-    module.execute_hook([], None, 'config.yaml', 'post-backup', dry_run=False)
+def test_execute_hooks_with_empty_commands_does_not_raise():
+    module.execute_hooks([], umask=None, dry_run=True)
 
 
 
 
-def test_execute_hook_on_error_logs_as_error():
+def test_execute_hooks_with_error_logs_as_error():
+    flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
+    flexmock(module.logging).ANSWER = LOGGING_ANSWER
     flexmock(module).should_receive('interpolate_context').replace_with(
     flexmock(module).should_receive('interpolate_context').replace_with(
         lambda hook_description, command, context: command
         lambda hook_description, command, context: command
     )
     )
     flexmock(module).should_receive('make_environment').and_return({})
     flexmock(module).should_receive('make_environment').and_return({})
     flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
     flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
-        [':'],
+        ['foo'],
         output_log_level=logging.ERROR,
         output_log_level=logging.ERROR,
         shell=True,
         shell=True,
         environment={},
         environment={},
     ).once()
     ).once()
 
 
-    module.execute_hook([':'], None, 'config.yaml', 'on-error', dry_run=False)
+    module.execute_hooks([{'after': 'error', 'run': ['foo']}], umask=None, dry_run=False)
 
 
 
 
 def test_considered_soft_failure_treats_soft_fail_exit_code_as_soft_fail():
 def test_considered_soft_failure_treats_soft_fail_exit_code_as_soft_fail():