diff --git a/setup.cfg b/setup.cfg index 60975408d..5c78cde38 100644 --- a/setup.cfg +++ b/setup.cfg @@ -121,6 +121,7 @@ per_file_ignores = src/borg/archiver/config.py:F405,E722 src/borg/archiver/common.py:E501,F405 src/borg/archiver/debug.py:F405 + src/borg/archiver/diff.py:F405 src/borg/archiver/help.py:E501,F405 src/borg/archiver/keys.py:F405 src/borg/archiver/prune.py:F405 diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index 5f170283c..1e12de7f5 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -100,6 +100,7 @@ def get_func(args): from .benchmarks import BenchmarkMixIn from .config import ConfigMixIn from .debug import DebugMixIn +from .diff import DiffMixIn from .help import HelpMixIn from .keys import KeysMixIn from .locks import LocksMixIn @@ -109,7 +110,16 @@ from .transfer import TransferMixIn class Archiver( - ConfigMixIn, DebugMixIn, TarMixIn, BenchmarkMixIn, KeysMixIn, LocksMixIn, PruneMixIn, HelpMixIn, TransferMixIn + ConfigMixIn, + DebugMixIn, + DiffMixIn, + TarMixIn, + BenchmarkMixIn, + KeysMixIn, + LocksMixIn, + PruneMixIn, + HelpMixIn, + TransferMixIn, ): def __init__(self, lock_wait=None, prog=None): self.exit_code = EXIT_SUCCESS @@ -774,50 +784,6 @@ class Archiver( pi.finish() return self.exit_code - @with_repository(compatibility=(Manifest.Operation.READ,)) - @with_archive - def do_diff(self, args, repository, manifest, key, archive): - """Diff contents of two archives""" - - def print_json_output(diff, path): - print(json.dumps({"path": path, "changes": [j for j, str in diff]})) - - def print_text_output(diff, path): - print("{:<19} {}".format(" ".join([str for j, str in diff]), path)) - - print_output = print_json_output if args.json_lines else print_text_output - - archive1 = archive - archive2 = Archive(repository, key, manifest, args.other_name, consider_part_files=args.consider_part_files) - - can_compare_chunk_ids = ( - archive1.metadata.get("chunker_params", False) == archive2.metadata.get("chunker_params", True) - or args.same_chunker_params - ) - if not can_compare_chunk_ids: - self.print_warning( - "--chunker-params might be different between archives, diff will be slow.\n" - "If you know for certain that they are the same, pass --same-chunker-params " - "to override this check." - ) - - matcher = self.build_matcher(args.patterns, args.paths) - - diffs = Archive.compare_archives_iter(archive1, archive2, matcher, can_compare_chunk_ids=can_compare_chunk_ids) - # Conversion to string and filtering for diff.equal to save memory if sorting - diffs = ((path, diff.changes()) for path, diff in diffs if not diff.equal) - - if args.sort: - diffs = sorted(diffs) - - for path, diff in diffs: - print_output(diff, path) - - for pattern in matcher.get_unmatched_include_patterns(): - self.print_warning("Include pattern '%s' never matched.", pattern) - - return self.exit_code - @with_repository(exclusive=True, cache=True, compatibility=(Manifest.Operation.CHECK,)) @with_archive def do_rename(self, args, repository, manifest, key, cache, archive): @@ -1627,6 +1593,7 @@ class Archiver( subparsers = parser.add_subparsers(title="required arguments", metavar="") self.build_parser_benchmarks(subparsers, common_parser, mid_common_parser) + self.build_parser_diff(subparsers, common_parser, mid_common_parser) self.build_parser_locks(subparsers, common_parser, mid_common_parser) self.build_parser_prune(subparsers, common_parser, mid_common_parser) @@ -2287,62 +2254,6 @@ class Archiver( ) define_archive_filters_group(subparser) - # borg diff - diff_epilog = process_epilog( - """ - This command finds differences (file contents, user/group/mode) between archives. - - A repository location and an archive name must be specified for REPO::ARCHIVE1. - ARCHIVE2 is just another archive name in same repository (no repository location - allowed). - - For archives created with Borg 1.1 or newer diff automatically detects whether - the archives are created with the same chunker params. If so, only chunk IDs - are compared, which is very fast. - - For archives prior to Borg 1.1 chunk contents are compared by default. - If you did not create the archives with different chunker params, - pass ``--same-chunker-params``. - Note that the chunker params changed from Borg 0.xx to 1.0. - - For more help on include/exclude patterns, see the :ref:`borg_patterns` command output. - """ - ) - subparser = subparsers.add_parser( - "diff", - parents=[common_parser], - add_help=False, - description=self.do_diff.__doc__, - epilog=diff_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, - help="find differences in archive contents", - ) - subparser.set_defaults(func=self.do_diff) - subparser.add_argument( - "--numeric-ids", - dest="numeric_ids", - action="store_true", - help="only consider numeric user and group identifiers", - ) - subparser.add_argument( - "--same-chunker-params", - dest="same_chunker_params", - action="store_true", - help="Override check of chunker parameters.", - ) - subparser.add_argument("--sort", dest="sort", action="store_true", help="Sort the output lines by file path.") - subparser.add_argument("--json-lines", action="store_true", help="Format output as JSON Lines. ") - subparser.add_argument("name", metavar="ARCHIVE1", type=archivename_validator(), help="ARCHIVE1 name") - subparser.add_argument("other_name", metavar="ARCHIVE2", type=archivename_validator(), help="ARCHIVE2 name") - subparser.add_argument( - "paths", - metavar="PATH", - nargs="*", - type=str, - help="paths of items inside the archives to compare; patterns are supported", - ) - define_exclusion_group(subparser) - # borg extract extract_epilog = process_epilog( """ diff --git a/src/borg/archiver/diff.py b/src/borg/archiver/diff.py new file mode 100644 index 000000000..d06ebe0f5 --- /dev/null +++ b/src/borg/archiver/diff.py @@ -0,0 +1,118 @@ +import argparse +import json + +from .common import with_repository, with_archive +from ..archive import Archive +from ..constants import * # NOQA +from ..helpers import archivename_validator +from ..helpers import Manifest + +from ..logger import create_logger + +logger = create_logger() + + +class DiffMixIn: + @with_repository(compatibility=(Manifest.Operation.READ,)) + @with_archive + def do_diff(self, args, repository, manifest, key, archive): + """Diff contents of two archives""" + + def print_json_output(diff, path): + print(json.dumps({"path": path, "changes": [j for j, str in diff]})) + + def print_text_output(diff, path): + print("{:<19} {}".format(" ".join([str for j, str in diff]), path)) + + print_output = print_json_output if args.json_lines else print_text_output + + archive1 = archive + archive2 = Archive(repository, key, manifest, args.other_name, consider_part_files=args.consider_part_files) + + can_compare_chunk_ids = ( + archive1.metadata.get("chunker_params", False) == archive2.metadata.get("chunker_params", True) + or args.same_chunker_params + ) + if not can_compare_chunk_ids: + self.print_warning( + "--chunker-params might be different between archives, diff will be slow.\n" + "If you know for certain that they are the same, pass --same-chunker-params " + "to override this check." + ) + + matcher = self.build_matcher(args.patterns, args.paths) + + diffs = Archive.compare_archives_iter(archive1, archive2, matcher, can_compare_chunk_ids=can_compare_chunk_ids) + # Conversion to string and filtering for diff.equal to save memory if sorting + diffs = ((path, diff.changes()) for path, diff in diffs if not diff.equal) + + if args.sort: + diffs = sorted(diffs) + + for path, diff in diffs: + print_output(diff, path) + + for pattern in matcher.get_unmatched_include_patterns(): + self.print_warning("Include pattern '%s' never matched.", pattern) + + return self.exit_code + + def build_parser_diff(self, subparsers, common_parser, mid_common_parser): + + from .common import process_epilog + from .common import define_exclusion_group + + diff_epilog = process_epilog( + """ + This command finds differences (file contents, user/group/mode) between archives. + + A repository location and an archive name must be specified for REPO::ARCHIVE1. + ARCHIVE2 is just another archive name in same repository (no repository location + allowed). + + For archives created with Borg 1.1 or newer diff automatically detects whether + the archives are created with the same chunker params. If so, only chunk IDs + are compared, which is very fast. + + For archives prior to Borg 1.1 chunk contents are compared by default. + If you did not create the archives with different chunker params, + pass ``--same-chunker-params``. + Note that the chunker params changed from Borg 0.xx to 1.0. + + For more help on include/exclude patterns, see the :ref:`borg_patterns` command output. + """ + ) + subparser = subparsers.add_parser( + "diff", + parents=[common_parser], + add_help=False, + description=self.do_diff.__doc__, + epilog=diff_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help="find differences in archive contents", + ) + subparser.set_defaults(func=self.do_diff) + subparser.add_argument( + "--numeric-ids", + dest="numeric_ids", + action="store_true", + help="only consider numeric user and group identifiers", + ) + subparser.add_argument( + "--same-chunker-params", + dest="same_chunker_params", + action="store_true", + help="Override check of chunker parameters.", + ) + subparser.add_argument("--sort", dest="sort", action="store_true", help="Sort the output lines by file path.") + subparser.add_argument("--json-lines", action="store_true", help="Format output as JSON Lines. ") + subparser.add_argument("name", metavar="ARCHIVE1", type=archivename_validator(), help="ARCHIVE1 name") + subparser.add_argument("other_name", metavar="ARCHIVE2", type=archivename_validator(), help="ARCHIVE2 name") + subparser.add_argument( + "paths", + metavar="PATH", + nargs="*", + type=str, + help="paths of items inside the archives to compare; patterns are supported", + ) + define_exclusion_group(subparser)