diff --git a/borgmatic/actions/diff.py b/borgmatic/actions/diff.py new file mode 100644 index 00000000..c6d3bb8b --- /dev/null +++ b/borgmatic/actions/diff.py @@ -0,0 +1,53 @@ +import logging + +import borgmatic.actions.pattern +import borgmatic.borg.pattern +import borgmatic.borg.diff + +logger = logging.getLogger(__name__) + + + +def run_diff( + repository, + config, + local_borg_version, + diff_arguments, + global_arguments, + local_path, + remote_path, +): + ''' + Run the "diff" action for the given repository. + ''' + + # Collect and process patterns. + processed_patterns = borgmatic.actions.pattern.process_patterns( + ( + *borgmatic.actions.pattern.collect_patterns(config), + ), + config, + borgmatic.config.paths.get_working_directory(config), + ) + + archive = borgmatic.borg.repo_list.resolve_archive_name( + repository['path'], + diff_arguments.archive, + config, + local_borg_version, + global_arguments, + local_path, + remote_path, + ) + + borgmatic.borg.diff.diff( + repository['path'], + archive, + config, + local_borg_version, + diff_arguments, + global_arguments, + local_path=local_path, + remote_path=remote_path, + patterns=processed_patterns, + ) diff --git a/borgmatic/borg/diff.py b/borgmatic/borg/diff.py new file mode 100644 index 00000000..52b92104 --- /dev/null +++ b/borgmatic/borg/diff.py @@ -0,0 +1,88 @@ +import logging +import shlex + +import borgmatic.borg.environment +import borgmatic.borg.feature +import borgmatic.config.paths +import borgmatic.execute +from borgmatic.borg import flags +from borgmatic.borg.pattern import write_patterns_file + +logger = logging.getLogger(__name__) + + +def diff( + repository, + archive, + config, + local_borg_version, + diff_arguments, + global_arguments, + local_path, + remote_path=None, + patterns=None, +): + ''' + Given a local or remote repository path, an archive name, a configuration dict, the local Borg + version string, an argparse.Namespace of diff arguments, an argparse.Namespace of global + arguments, optional local and remote Borg paths, executes the diff command with the given + arguments. + ''' + lock_wait = config.get('lock_wait', None) + exclude_flags = flags.make_exclude_flags(config) + extra_borg_options = config.get('extra_borg_options', {}).get('diff', '') + + # Write patterns to a temporary file and use that file with --patterns-from. + patterns_file = write_patterns_file( + patterns, + borgmatic.config.paths.get_working_directory(config), + ) + + diff_command = ( + (local_path, 'diff') + + (('--remote-path', remote_path) if remote_path else ()) + + ('--log-json',) + + (('--lock-wait', str(lock_wait)) if lock_wait is not None else ()) + + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ()) + + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ()) + + (('--patterns-from', patterns_file.name) if patterns_file else ()) + + ( + ( + '--list', + '--filter', + flags.make_list_filter_flags(local_borg_version, global_arguments.dry_run), + ) + if config.get('list_details') + else () + ) + + exclude_flags + + (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ()) + + ( + ( + flags.make_repository_flags(repository, local_borg_version) + + flags.make_match_archives_flags( + archive or config.get('match_archives'), + config.get('archive_name_format'), + local_borg_version, + ) + ) + if borgmatic.borg.feature.available( + borgmatic.borg.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, + local_borg_version, + ) + else ( + flags.make_repository_archive_flags(repository, archive, local_borg_version) + if archive + else flags.make_repository_flags(repository, local_borg_version) + ) + ) + ) + + borgmatic.execute.execute_command( + full_command=diff_command, + output_log_level=logging.INFO, + environment=borgmatic.borg.environment.make_environment(config), + working_directory=borgmatic.config.paths.get_working_directory(config), + borg_local_path=local_path, + borg_exit_codes=config.get('borg_exit_codes'), + ) diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 58c3a632..0b7df4e4 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -33,6 +33,7 @@ ACTION_ALIASES = { 'key': [], 'borg': [], 'recreate': [], + 'diff': [], } @@ -1970,6 +1971,23 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915 help='Show this help message and exit', ) + diff_parser = action_parsers.add_parser( + 'diff', + aliases=ACTION_ALIASES['diff'], + help='This command finds differences (file contents, user/group/mode) between archives', + description='This command finds differences (file contents, user/group/mode) between archives', + add_help=False, + ) + diff_group = diff_parser.add_argument_group('diff arguments') + diff_group.add_argument( + '--repository', + help='Path of repository containing archive to diff, defaults to the configured repository if there is only one, quoted globs supported', + ) + diff_group.add_argument( + '--archive', + help='Archive name, hash, or series to diff, defaults to all archives in the repository (if specified)', + ) + borg_parser = action_parsers.add_parser( 'borg', aliases=ACTION_ALIASES['borg'], diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 2954ac72..ffab7219 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -20,6 +20,7 @@ import borgmatic.actions.config.generate import borgmatic.actions.config.validate import borgmatic.actions.create import borgmatic.actions.delete +import borgmatic.actions.diff import borgmatic.actions.export_key import borgmatic.actions.export_tar import borgmatic.actions.extract @@ -622,6 +623,16 @@ def run_actions( # noqa: PLR0912, PLR0915 local_path, remote_path, ) + elif action_name == 'diff': + borgmatic.actions.diff.run_diff( + repository, + config, + local_borg_version, + action_arguments, + global_arguments, + local_path, + remote_path, + ) elif action_name == 'borg': borgmatic.actions.borg.run_borg( repository,