diff --git a/borg/archiver.py b/borg/archiver.py index 29c39a2a6..146adc316 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -514,7 +514,7 @@ def compare_mode(item1, item2): if item1[b'mode'] != item2[b'mode']: return '[{} -> {}]'.format(get_mode(item1), get_mode(item2)) - def compare_items(path, item1, item2, hardlink_masters, deleted=False): + def compare_items(output, path, item1, item2, hardlink_masters, deleted=False): """ Compare two items with identical paths. :param deleted: Whether one of the items has been deleted @@ -545,43 +545,56 @@ def compare_items(path, item1, item2, hardlink_masters, deleted=False): changes = [x for x in changes if x] if changes: - print("{:<19} {}".format(' '.join(changes), remove_surrogates(path))) + output_line = (remove_surrogates(path), ' '.join(changes)) + + if args.sort: + output.append(output_line) + else: + print_output(output_line) + + def print_output(line): + print("{:<19} {}".format(line[1], line[0])) def compare_archives(archive1, archive2, matcher): orphans_archive1 = collections.OrderedDict() orphans_archive2 = collections.OrderedDict() hardlink_masters = {} + output = [] + for item1, item2 in zip_longest( archive1.iter_items(lambda item: matcher.match(item[b'path'])), archive2.iter_items(lambda item: matcher.match(item[b'path'])), ): if item1 and item2 and item1[b'path'] == item2[b'path']: - compare_items(item1[b'path'], item1, item2, hardlink_masters) + compare_items(output, item1[b'path'], item1, item2, hardlink_masters) continue if item1: matching_orphan = orphans_archive2.pop(item1[b'path'], None) if matching_orphan: - compare_items(item1[b'path'], item1, matching_orphan, hardlink_masters) + compare_items(output, item1[b'path'], item1, matching_orphan, hardlink_masters) else: orphans_archive1[item1[b'path']] = item1 if item2: matching_orphan = orphans_archive1.pop(item2[b'path'], None) if matching_orphan: - compare_items(item2[b'path'], matching_orphan, item2, hardlink_masters) + compare_items(output, item2[b'path'], matching_orphan, item2, hardlink_masters) else: orphans_archive2[item2[b'path']] = item2 # At this point orphans_* contain items that had no matching partner in the other archive for added in orphans_archive2.values(): - compare_items(added[b'path'], { + compare_items(output, added[b'path'], { b'deleted': True, b'chunks': [], }, added, hardlink_masters, deleted=True) for deleted in orphans_archive1.values(): - compare_items(deleted[b'path'], deleted, { + compare_items(output, deleted[b'path'], deleted, { b'deleted': True, b'chunks': [], }, hardlink_masters, deleted=True) + for line in sorted(output): + print_output(line) + archive1 = archive archive2 = Archive(repository, key, manifest, args.archive2) @@ -1308,6 +1321,9 @@ def build_parser(self, args=None, prog=None): subparser.add_argument('--same-chunker-params', dest='same_chunker_params', action='store_true', default=False, help='Override check of chunker parameters.') + subparser.add_argument('--sort', dest='sort', + action='store_true', default=False, + help='Sort the output lines by file path.') subparser.add_argument('location', metavar='ARCHIVE1', type=location_validator(archive=True), help='archive') diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 0b71d96e4..ed8ecf1c0 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1351,6 +1351,37 @@ def do_asserts(output, archive): # We expect exit_code=1 due to the chunker params warning do_asserts(self.cmd('diff', self.repository_location + '::test0', 'test1b', exit_code=1), '1b') + def test_sort_option(self): + self.cmd('init', self.repository_location) + + self.create_regular_file('a_file_removed', size=8) + self.create_regular_file('f_file_removed', size=16) + self.create_regular_file('c_file_changed', size=32) + self.create_regular_file('e_file_changed', size=64) + self.cmd('create', self.repository_location + '::test0', 'input') + + os.unlink('input/a_file_removed') + os.unlink('input/f_file_removed') + os.unlink('input/c_file_changed') + os.unlink('input/e_file_changed') + self.create_regular_file('c_file_changed', size=512) + self.create_regular_file('e_file_changed', size=1024) + self.create_regular_file('b_file_added', size=128) + self.create_regular_file('d_file_added', size=256) + self.cmd('create', self.repository_location + '::test1', 'input') + + output = self.cmd('diff', '--sort', self.repository_location + '::test0', 'test1') + expected = [ + 'a_file_removed', + 'b_file_added', + 'c_file_changed', + 'd_file_added', + 'e_file_changed', + 'f_file_removed', + ] + + assert all(x in line for x, line in zip(expected, output.splitlines())) + def test_get_args(): archiver = Archiver() diff --git a/docs/usage/diff.rst.inc b/docs/usage/diff.rst.inc index ba07b8203..becb144ad 100644 --- a/docs/usage/diff.rst.inc +++ b/docs/usage/diff.rst.inc @@ -8,7 +8,7 @@ borg diff [--show-rc] [--no-files-cache] [--umask M] [--remote-path PATH] [-e PATTERN] [--exclude-from EXCLUDEFILE] [--numeric-owner] - [--same-chunker-params] + [--same-chunker-params] [--sort] ARCHIVE1 ARCHIVE2 [PATH [PATH ...]] Diff contents of two archives @@ -39,6 +39,7 @@ borg diff --numeric-owner only consider numeric user and group identifiers --same-chunker-params Override check of chunker parameters. + --sort Sort the output lines by file path. Description ~~~~~~~~~~~