Repository: borgbackup/borg

Issue:
--timestamp time (vs list time)
The time set with `--timestamp` is the end time, whereas `list` shows the start time, e.g.:

```
$ borg create --timestamp 2017-01-01T00:00:00 ::foo bar
$ borg list
foo                                  Sat, 2016-12-31 23:59:59
$ borg info ::foo
[...]
Time (start): Sat, 2016-12-31 23:59:59
Time (end):   Sun, 2017-01-01 00:00:00
Command line: borg create --timestamp 2017-01-01T00:00:00 ::foo bar
[...]
```

Having a start and end time is weird with only a single timestamp given. But if current design should not be changed and the start time is considered the important one, that's what `--timestamp` should set.

Discussion:

Patch:
diff --git a/docs/changes.rst b/docs/changes.rst
index 9d32ccfd..ec436b7e 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -139,12 +139,7 @@ Compatibility notes:
 - dropped support and testing for Python 3.4, minimum requirement is 3.5.0.
   In case your OS does not provide Python >= 3.5, consider using our binary,
   which does not need an external Python interpreter.
-- list: corrected mix-up of "isomtime" and "mtime" formats. Previously,
-  "isomtime" was the default but produced a verbose human format,
-  while "mtime" produced a ISO-8601-like format.
-  The behaviours have been swapped (so "mtime" is human, "isomtime" is ISO-like),
-  and the default is now "mtime".
-  "isomtime" is now a real ISO-8601 format ("T" between date and time, not a space).
+
 
 Version 1.1.0rc1 (2017-07-24)
 -----------------------------
diff --git a/docs/development.rst b/docs/development.rst
index 365025d9..e9868a1b 100644
--- a/docs/development.rst
+++ b/docs/development.rst
@@ -17,7 +17,7 @@ Contributions
 
 Some guidance for contributors:
 
-- discuss changes on the GitHub issue tracker, on IRC or on the mailing list
+- discuss about changes on github issue tracker, IRC or mailing list
 
 - make your PRs on the ``master`` branch (see `Branching Model`_ for details)
 
@@ -29,9 +29,9 @@ Some guidance for contributors:
   - if you need to fix something after commit/push:
 
     - if there are ongoing reviews: do a fixup commit you can
-      squash into the bad commit later.
+      merge into the bad commit later.
     - if there are no ongoing reviews or you did not push the
-      bad commit yet: amend the commit to include your fix or
+      bad commit yet: edit the commit to include your fix or
       merge the fixup commit before pushing.
   - have a nice, clear, typo-free commit comment
   - if you fixed an issue, refer to it in your commit comment
@@ -39,9 +39,9 @@ Some guidance for contributors:
 
 - if you write new code, please add tests and docs for it
 
-- run the tests, fix any issues that come up
+- run the tests, fix anything that comes up
 
-- make a pull request on GitHub
+- make a pull request on github
 
 - wait for review by other developers
 
@@ -52,15 +52,15 @@ Borg development happens on the ``master`` branch and uses GitHub pull
 requests (if you don't have GitHub or don't want to use it you can
 send smaller patches via the borgbackup :ref:`mailing_list` to the maintainers).
 
-Stable releases are maintained on maintenance branches named ``x.y-maint``, eg.
-the maintenance branch of the 1.0.x series is ``1.0-maint``.
+Stable releases are maintained on maintenance branches named x.y-maint, eg.
+the maintenance branch of the 1.0.x series is 1.0-maint.
 
-Most PRs should be filed against the ``master`` branch. Only if an
+Most PRs should be made against the ``master`` branch. Only if an
 issue affects **only** a particular maintenance branch a PR should be
-filed against it directly.
+made against it directly.
 
 While discussing / reviewing a PR it will be decided whether the
-change should be applied to maintenance branches. Each maintenance
+change should be applied to maintenance branch(es). Each maintenance
 branch has a corresponding *backport/x.y-maint* label, which will then
 be applied.
 
@@ -113,7 +113,7 @@ troublesome due to merges growing more conflict-heavy and error-prone.
 Code and issues
 ---------------
 
-Code is stored on GitHub, in the `Borgbackup organization
+Code is stored on Github, in the `Borgbackup organization
 <https://github.com/borgbackup/borg/>`_. `Issues
 <https://github.com/borgbackup/borg/issues>`_ and `pull requests
 <https://github.com/borgbackup/borg/pulls>`_ should be sent there as
@@ -222,7 +222,7 @@ parsers declared in the program and their documentation, which is
 embedded in the program (see archiver.py). These are committed to git
 for easier use by packagers downstream.
 
-When a command is added, a command line flag changed, added or removed,
+When a command is added, a commandline flag changed, added or removed,
 the usage docs need to be rebuilt as well::
 
   python setup.py build_usage
@@ -327,8 +327,8 @@ Checklist:
     git clone borg borg-clean
 
   This makes sure no uncommitted files get into the release archive.
-  It will also reveal uncommitted required files.
-  Moreover, it makes sure the vagrant machines only get committed files and
+  It also will find if you forgot to commit something that is needed.
+  It also makes sure the vagrant machines only get committed files and
   do a fresh start based on that.
 - run tox and/or binary builds on all supported platforms via vagrant,
   check for test failures
@@ -336,14 +336,14 @@ Checklist:
 
     python setup.py register sdist upload --identity="Thomas Waldmann" --sign
 
-- close the release milestone on GitHub
+- close release milestone on Github
 - announce on:
 
  - Mailing list
  - Twitter (follow @ThomasJWaldmann for these tweets)
  - IRC channel (change ``/topic``)
 
-- create a GitHub release, include:
+- create a Github release, include:
 
   * standalone binaries (see above for how to create them)
 
diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst
index 045b476b..dd7eaefd 100644
--- a/docs/internals/data-structures.rst
+++ b/docs/internals/data-structures.rst
@@ -96,7 +96,7 @@ files manually.
 A segment starts with a magic number (``BORG_SEG`` as an eight byte ASCII string),
 followed by a number of log entries. Each log entry consists of:
 
-* 32-bit size of the entry
+* size of the entry
 * CRC32 of the entire entry (for a PUT this includes the data)
 * entry tag: PUT, DELETE or COMMIT
 * PUT and DELETE follow this with the 32 byte key
@@ -118,9 +118,6 @@ When a repository is opened any ``PUT`` or ``DELETE`` operations not
 followed by a ``COMMIT`` tag are discarded since they are part of a
 partial/uncommitted transaction.
 
-The size of individual segments is limited to 4 GiB, since the offset of entries
-within segments is stored in a 32-bit unsigned integer in the repository index.
-
 Index, hints and integrity
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst
index cfe9730f..677db738 100644
--- a/docs/internals/frontends.rst
+++ b/docs/internals/frontends.rst
@@ -209,9 +209,8 @@ Standard output
 *stdout* is different and more command-dependent than logging. Commands like :ref:`borg_info`, :ref:`borg_create`
 and :ref:`borg_list` implement a ``--json`` option which turns their regular output into a single JSON object.
 
-Dates are formatted according to ISO 8601 in local time. No explicit time zone is specified *at this time*
-(subject to change). The equivalent strftime format string is '%Y-%m-%dT%H:%M:%S.%f',
-e.g. ``2017-08-07T12:27:20.123456``.
+Dates are formatted according to ISO-8601 with the strftime format string '%a, %Y-%m-%d %H:%M:%S',
+e.g. *Sat, 2016-02-25 23:50:06*.
 
 The root object at least contains a *repository* key with an object containing:
 
@@ -268,7 +267,7 @@ Example *borg info* output::
         },
         "repository": {
             "id": "0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23",
-            "last_modified": "2017-08-07T12:27:20.789123",
+            "last_modified": "Mon, 2017-02-27 21:21:58",
             "location": "/home/user/testrepo"
         },
         "security_dir": "/home/user/.config/borg/security/0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23",
@@ -329,7 +328,7 @@ Example of a simple archive listing (``borg list --last 1 --json``)::
             {
                 "id": "80cd07219ad725b3c5f665c1dcf119435c4dee1647a560ecac30f8d40221a46a",
                 "name": "host-system-backup-2017-02-27",
-                "start": "2017-08-07T12:27:20.789123"
+                "start": "Mon, 2017-02-27 21:21:52"
             }
         ],
         "encryption": {
@@ -337,7 +336,7 @@ Example of a simple archive listing (``borg list --last 1 --json``)::
         },
         "repository": {
             "id": "0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23",
-            "last_modified": "2017-08-07T12:27:20.789123",
+            "last_modified": "Mon, 2017-02-27 21:21:58",
             "location": "/home/user/repository"
         }
     }
@@ -355,14 +354,14 @@ The same archive with more information (``borg info --last 1 --json``)::
                 ],
                 "comment": "",
                 "duration": 5.641542,
-                "end": "2017-02-27T12:27:20.789123",
+                "end": "Mon, 2017-02-27 21:21:58",
                 "hostname": "host",
                 "id": "80cd07219ad725b3c5f665c1dcf119435c4dee1647a560ecac30f8d40221a46a",
                 "limits": {
                     "max_archive_size": 0.0001330855110409714
                 },
                 "name": "host-system-backup-2017-02-27",
-                "start": "2017-02-27T12:27:20.789123",
+                "start": "Mon, 2017-02-27 21:21:52",
                 "stats": {
                     "compressed_size": 1880961894,
                     "deduplicated_size": 2791,
@@ -388,7 +387,7 @@ The same archive with more information (``borg info --last 1 --json``)::
         },
         "repository": {
             "id": "0cbe6166b46627fd26b97f8831e2ca97584280a46714ef84d2b668daf8271a23",
-            "last_modified": "2017-08-07T12:27:20.789123",
+            "last_modified": "Mon, 2017-02-27 21:21:58",
             "location": "/home/user/repository"
         }
     }
@@ -406,8 +405,8 @@ Refer to the *borg list* documentation for the available keys and their meaning.
 
 Example (excerpt) of ``borg list --json-lines``::
 
-    {"type": "d", "mode": "drwxr-xr-x", "user": "user", "group": "user", "uid": 1000, "gid": 1000, "path": "linux", "healthy": true, "source": "", "linktarget": "", "flags": null, "mtime": "2017-02-27T12:27:20.023407", "size": 0}
-    {"type": "d", "mode": "drwxr-xr-x", "user": "user", "group": "user", "uid": 1000, "gid": 1000, "path": "linux/baz", "healthy": true, "source": "", "linktarget": "", "flags": null, "mtime": "2017-02-27T12:27:20.585407", "size": 0}
+    {"type": "d", "mode": "drwxr-xr-x", "user": "user", "group": "user", "uid": 1000, "gid": 1000, "path": "linux", "healthy": true, "source": "", "linktarget": "", "flags": null, "isomtime": "Sat, 2016-05-07 19:46:01", "size": 0}
+    {"type": "d", "mode": "drwxr-xr-x", "user": "user", "group": "user", "uid": 1000, "gid": 1000, "path": "linux/baz", "healthy": true, "source": "", "linktarget": "", "flags": null, "isomtime": "Sat, 2016-05-07 19:46:01", "size": 0}
 
 .. _msgid:
 
diff --git a/src/borg/archive.py b/src/borg/archive.py
index 4f25666f..a87219e0 100644
--- a/src/borg/archive.py
+++ b/src/borg/archive.py
@@ -5,13 +5,12 @@
 import stat
 import sys
 import time
-from collections import OrderedDict
 from contextlib import contextmanager
 from datetime import datetime, timezone, timedelta
 from functools import partial
 from getpass import getuser
 from io import BytesIO
-from itertools import groupby, zip_longest
+from itertools import groupby
 from shutil import get_terminal_size
 
 import msgpack
@@ -34,14 +33,14 @@
 from .helpers import Error, IntegrityError, set_ec
 from .helpers import uid2user, user2uid, gid2group, group2gid
 from .helpers import parse_timestamp, to_localtime
-from .helpers import OutputTimestamp, format_timedelta, format_file_size, file_status, FileSize
+from .helpers import format_time, format_timedelta, format_file_size, file_status, FileSize
 from .helpers import safe_encode, safe_decode, make_path_safe, remove_surrogates
 from .helpers import StableDict
 from .helpers import bin_to_hex
 from .helpers import safe_ns
 from .helpers import ellipsis_truncate, ProgressIndicatorPercent, log_multi
 from .patterns import PathPrefixPattern, FnmatchPattern, IECommand
-from .item import Item, ArchiveItem, ItemDiff
+from .item import Item, ArchiveItem
 from .platform import acl_get, acl_set, set_flags, get_flags, swidth
 from .remote import cache_if_remote
 from .repository import Repository, LIST_SCAN_LIMIT
@@ -381,8 +380,8 @@ def info(self):
         info = {
             'name': self.name,
             'id': self.fpr,
-            'start': OutputTimestamp(start),
-            'end': OutputTimestamp(end),
+            'start': format_time(to_localtime(start)),
+            'end': format_time(to_localtime(end)),
             'duration': (end - start).total_seconds(),
             'stats': stats.as_dict(),
             'limits': {
@@ -411,8 +410,8 @@ def __str__(self):
 Utilization of max. archive size: {csize_max:.0%}
 '''.format(
             self,
-            start=OutputTimestamp(self.start.replace(tzinfo=timezone.utc)),
-            end=OutputTimestamp(self.end.replace(tzinfo=timezone.utc)),
+            start=format_time(to_localtime(self.start.replace(tzinfo=timezone.utc))),
+            end=format_time(to_localtime(self.end.replace(tzinfo=timezone.utc))),
             csize_max=self.cache.chunks[self.id].csize / MAX_DATA_SIZE)
 
     def __repr__(self):
@@ -446,15 +445,13 @@ def save(self, name=None, comment=None, timestamp=None, additional_metadata=None
         self.items_buffer.flush(flush=True)
         duration = timedelta(seconds=time.monotonic() - self.start_monotonic)
         if timestamp is None:
-            self.end = datetime.utcnow()
-            self.start = self.end - duration
-            start = self.start
-            end = self.end
+            end = datetime.utcnow()
+            start = end - duration
         else:
-            self.end = timestamp
-            self.start = timestamp - duration
-            end = timestamp
-            start = self.start
+            end = timestamp + duration
+            start = timestamp
+        self.start = start
+        self.end = end
         metadata = {
             'version': 1,
             'name': name,
@@ -820,89 +817,6 @@ def _open_rb(path):
             # Was this EPERM due to the O_NOATIME flag? Try again without it:
             return os.open(path, flags_normal)
 
-    @staticmethod
-    def compare_archives_iter(archive1, archive2, matcher=None, can_compare_chunk_ids=False):
-        """
-        Yields tuples with a path and an ItemDiff instance describing changes/indicating equality.
-
-        :param matcher: PatternMatcher class to restrict results to only matching paths.
-        :param can_compare_chunk_ids: Whether --chunker-params are the same for both archives.
-        """
-
-        def hardlink_master_seen(item):
-            return 'source' not in item or not hardlinkable(item.mode) or item.source in hardlink_masters
-
-        def is_hardlink_master(item):
-            return item.get('hardlink_master', True) and 'source' not in item
-
-        def update_hardlink_masters(item1, item2):
-            if is_hardlink_master(item1) or is_hardlink_master(item2):
-                hardlink_masters[item1.path] = (item1, item2)
-
-        def has_hardlink_master(item, hardlink_masters):
-            return hardlinkable(item.mode) and item.get('source') in hardlink_masters
-
-        def compare_items(item1, item2):
-            if has_hardlink_master(item1, hardlink_masters):
-                item1 = hardlink_masters[item1.source][0]
-            if has_hardlink_master(item2, hardlink_masters):
-                item2 = hardlink_masters[item2.source][1]
-            return ItemDiff(item1, item2,
-                            archive1.pipeline.fetch_many([c.id for c in item1.get('chunks', [])]),
-                            archive2.pipeline.fetch_many([c.id for c in item2.get('chunks', [])]),
-                            can_compare_chunk_ids=can_compare_chunk_ids)
-
-        def defer_if_necessary(item1, item2):
-            """Adds item tuple to deferred if necessary and returns True, if items were deferred"""
-            update_hardlink_masters(item1, item2)
-            defer = not hardlink_master_seen(item1) or not hardlink_master_seen(item2)
-            if defer:
-                deferred.append((item1, item2))
-            return defer
-
-        orphans_archive1 = OrderedDict()
-        orphans_archive2 = OrderedDict()
-        deferred = []
-        hardlink_masters = {}
-
-        for item1, item2 in zip_longest(
-                archive1.iter_items(lambda item: matcher.match(item.path)),
-                archive2.iter_items(lambda item: matcher.match(item.path)),
-        ):
-            if item1 and item2 and item1.path == item2.path:
-                if not defer_if_necessary(item1, item2):
-                    yield (item1.path, compare_items(item1, item2))
-                continue
-            if item1:
-                matching_orphan = orphans_archive2.pop(item1.path, None)
-                if matching_orphan:
-                    if not defer_if_necessary(item1, matching_orphan):
-                        yield (item1.path, compare_items(item1, matching_orphan))
-                else:
-                    orphans_archive1[item1.path] = item1
-            if item2:
-                matching_orphan = orphans_archive1.pop(item2.path, None)
-                if matching_orphan:
-                    if not defer_if_necessary(matching_orphan, item2):
-                        yield (matching_orphan.path, compare_items(matching_orphan, item2))
-                else:
-                    orphans_archive2[item2.path] = item2
-        # At this point orphans_* contain items that had no matching partner in the other archive
-        for added in orphans_archive2.values():
-            path = added.path
-            deleted_item = Item.create_deleted(path)
-            update_hardlink_masters(deleted_item, added)
-            yield (path, compare_items(deleted_item, added))
-        for deleted in orphans_archive1.values():
-            path = deleted.path
-            deleted_item = Item.create_deleted(path)
-            update_hardlink_masters(deleted, deleted_item)
-            yield (path, compare_items(deleted, deleted_item))
-        for item1, item2 in deferred:
-            assert hardlink_master_seen(item1)
-            assert hardlink_master_seen(item2)
-            yield (path, compare_items(item1, item2))
-
 
 class MetadataCollector:
     def __init__(self, *, noatime, noctime, numeric_owner):
diff --git a/src/borg/archiver.py b/src/borg/archiver.py
index 48b9fde7..80fd7887 100644
--- a/src/borg/archiver.py
+++ b/src/borg/archiver.py
@@ -195,6 +195,33 @@ def print_file_status(self, status, path):
             else:
                 logging.getLogger('borg.output.list').info("%1s %s", status, remove_surrogates(path))
 
+    @staticmethod
+    def compare_chunk_contents(chunks1, chunks2):
+        """Compare two chunk iterators (like returned by :meth:`.DownloadPipeline.fetch_many`)"""
+        end = object()
+        alen = ai = 0
+        blen = bi = 0
+        while True:
+            if not alen - ai:
+                a = next(chunks1, end)
+                if a is end:
+                    return not blen - bi and next(chunks2, end) is end
+                a = memoryview(a)
+                alen = len(a)
+                ai = 0
+            if not blen - bi:
+                b = next(chunks2, end)
+                if b is end:
+                    return not alen - ai and next(chunks1, end) is end
+                b = memoryview(b)
+                blen = len(b)
+                bi = 0
+            slicelen = min(alen - ai, blen - bi)
+            if a[ai:ai + slicelen] != b[bi:bi + slicelen]:
+                return False
+            ai += slicelen
+            bi += slicelen
+
     @staticmethod
     def build_matcher(inclexcl_patterns, include_paths):
         matcher = PatternMatcher()
@@ -940,9 +967,195 @@ def item_to_tarinfo(item, original_path):
     @with_archive
     def do_diff(self, args, repository, manifest, key, archive):
         """Diff contents of two archives"""
+        def fetch_and_compare_chunks(chunk_ids1, chunk_ids2, archive1, archive2):
+            chunks1 = archive1.pipeline.fetch_many(chunk_ids1)
+            chunks2 = archive2.pipeline.fetch_many(chunk_ids2)
+            return self.compare_chunk_contents(chunks1, chunks2)
+
+        def sum_chunk_size(item, consider_ids=None):
+            if item.get('deleted'):
+                size = None
+            else:
+                if consider_ids is not None:  # consider only specific chunks
+                    size = sum(chunk.size for chunk in item.chunks if chunk.id in consider_ids)
+                else:  # consider all chunks
+                    size = item.get_size()
+            return size
+
+        def get_owner(item):
+            if args.numeric_owner:
+                return item.uid, item.gid
+            else:
+                return item.user, item.group
+
+        def get_mode(item):
+            if 'mode' in item:
+                return stat.filemode(item.mode)
+            else:
+                return [None]
+
+        def has_hardlink_master(item, hardlink_masters):
+            return hardlinkable(item.mode) and item.get('source') in hardlink_masters
+
+        def compare_link(item1, item2):
+            # These are the simple link cases. For special cases, e.g. if a
+            # regular file is replaced with a link or vice versa, it is
+            # indicated in compare_mode instead.
+            if item1.get('deleted'):
+                return 'added link'
+            elif item2.get('deleted'):
+                return 'removed link'
+            elif 'source' in item1 and 'source' in item2 and item1.source != item2.source:
+                return 'changed link'
+
+        def contents_changed(item1, item2):
+            if can_compare_chunk_ids:
+                return item1.chunks != item2.chunks
+            else:
+                if sum_chunk_size(item1) != sum_chunk_size(item2):
+                    return True
+                else:
+                    chunk_ids1 = [c.id for c in item1.chunks]
+                    chunk_ids2 = [c.id for c in item2.chunks]
+                    return not fetch_and_compare_chunks(chunk_ids1, chunk_ids2, archive1, archive2)
+
+        def compare_content(path, item1, item2):
+            if contents_changed(item1, item2):
+                if item1.get('deleted'):
+                    return 'added {:>13}'.format(format_file_size(sum_chunk_size(item2)))
+                if item2.get('deleted'):
+                    return 'removed {:>11}'.format(format_file_size(sum_chunk_size(item1)))
+                if not can_compare_chunk_ids:
+                    return 'modified'
+                chunk_ids1 = {c.id for c in item1.chunks}
+                chunk_ids2 = {c.id for c in item2.chunks}
+                added_ids = chunk_ids2 - chunk_ids1
+                removed_ids = chunk_ids1 - chunk_ids2
+                added = sum_chunk_size(item2, added_ids)
+                removed = sum_chunk_size(item1, removed_ids)
+                return '{:>9} {:>9}'.format(format_file_size(added, precision=1, sign=True),
+                                            format_file_size(-removed, precision=1, sign=True))
+
+        def compare_directory(item1, item2):
+            if item2.get('deleted') and not item1.get('deleted'):
+                return 'removed directory'
+            elif item1.get('deleted') and not item2.get('deleted'):
+                return 'added directory'
+
+        def compare_owner(item1, item2):
+            user1, group1 = get_owner(item1)
+            user2, group2 = get_owner(item2)
+            if user1 != user2 or group1 != group2:
+                return '[{}:{} -> {}:{}]'.format(user1, group1, user2, group2)
+
+        def compare_mode(item1, item2):
+            if item1.mode != item2.mode:
+                return '[{} -> {}]'.format(get_mode(item1), get_mode(item2))
+
+        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
+            """
+            changes = []
+
+            if has_hardlink_master(item1, hardlink_masters):
+                item1 = hardlink_masters[item1.source][0]
+
+            if has_hardlink_master(item2, hardlink_masters):
+                item2 = hardlink_masters[item2.source][1]
+
+            if get_mode(item1)[0] == 'l' or get_mode(item2)[0] == 'l':
+                changes.append(compare_link(item1, item2))
+
+            if 'chunks' in item1 and 'chunks' in item2:
+                changes.append(compare_content(path, item1, item2))
+
+            if get_mode(item1)[0] == 'd' or get_mode(item2)[0] == 'd':
+                changes.append(compare_directory(item1, item2))
 
-        def print_output(diff, path):
-            print("{:<19} {}".format(diff, path))
+            if not deleted:
+                changes.append(compare_owner(item1, item2))
+                changes.append(compare_mode(item1, item2))
+
+            changes = [x for x in changes if x]
+            if changes:
+                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):
+            def hardlink_master_seen(item):
+                return 'source' not in item or not hardlinkable(item.mode) or item.source in hardlink_masters
+
+            def is_hardlink_master(item):
+                return item.get('hardlink_master', True) and 'source' not in item
+
+            def update_hardlink_masters(item1, item2):
+                if is_hardlink_master(item1) or is_hardlink_master(item2):
+                    hardlink_masters[item1.path] = (item1, item2)
+
+            def compare_or_defer(item1, item2):
+                update_hardlink_masters(item1, item2)
+                if not hardlink_master_seen(item1) or not hardlink_master_seen(item2):
+                    deferred.append((item1, item2))
+                else:
+                    compare_items(output, item1.path, item1, item2, hardlink_masters)
+
+            orphans_archive1 = collections.OrderedDict()
+            orphans_archive2 = collections.OrderedDict()
+            deferred = []
+            hardlink_masters = {}
+            output = []
+
+            for item1, item2 in zip_longest(
+                    archive1.iter_items(lambda item: matcher.match(item.path)),
+                    archive2.iter_items(lambda item: matcher.match(item.path)),
+            ):
+                if item1 and item2 and item1.path == item2.path:
+                    compare_or_defer(item1, item2)
+                    continue
+                if item1:
+                    matching_orphan = orphans_archive2.pop(item1.path, None)
+                    if matching_orphan:
+                        compare_or_defer(item1, matching_orphan)
+                    else:
+                        orphans_archive1[item1.path] = item1
+                if item2:
+                    matching_orphan = orphans_archive1.pop(item2.path, None)
+                    if matching_orphan:
+                        compare_or_defer(matching_orphan, item2)
+                    else:
+                        orphans_archive2[item2.path] = item2
+            # At this point orphans_* contain items that had no matching partner in the other archive
+            deleted_item = Item(
+                deleted=True,
+                chunks=[],
+                mode=0,
+            )
+            for added in orphans_archive2.values():
+                path = added.path
+                deleted_item.path = path
+                update_hardlink_masters(deleted_item, added)
+                compare_items(output, path, deleted_item, added, hardlink_masters, deleted=True)
+            for deleted in orphans_archive1.values():
+                path = deleted.path
+                deleted_item.path = path
+                update_hardlink_masters(deleted, deleted_item)
+                compare_items(output, path, deleted, deleted_item, hardlink_masters, deleted=True)
+            for item1, item2 in deferred:
+                assert hardlink_master_seen(item1)
+                assert hardlink_master_seen(item2)
+                compare_items(output, item1.path, item1, item2, hardlink_masters)
+
+            for line in sorted(output):
+                print_output(line)
 
         archive1 = archive
         archive2 = Archive(repository, key, manifest, args.archive2,
@@ -957,15 +1170,7 @@ def print_output(diff, path):
 
         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, str(diff)) for path, diff in diffs if not diff.equal)
-
-        if args.sort:
-            diffs = sorted(diffs)
-
-        for path, diff in diffs:
-            print_output(diff, path)
+        compare_archives(archive1, archive2, matcher)
 
         for pattern in matcher.get_unmatched_include_patterns():
             self.print_warning("Include pattern '%s' never matched.", pattern)
@@ -1137,7 +1342,7 @@ def _list_archive(self, args, repository, manifest, key, write):
         elif args.short:
             format = "{path}{NL}"
         else:
-            format = "{mode} {user:6} {group:6} {size:8} {mtime} {path}{extra}{NL}"
+            format = "{mode} {user:6} {group:6} {size:8} {isomtime} {path}{extra}{NL}"
 
         def _list_inner(cache):
             archive = Archive(repository, key, manifest, args.location.archive, cache=cache,
@@ -2610,9 +2815,6 @@ def define_archive_filters_group(subparser, *, sort_by=True, first_last=True):
         and not include any other contents of the containing folder, this can be enabled
         through using the ``--keep-exclude-tags`` option.
 
-        Borg respects the nodump flag. Files flagged nodump will be marked as excluded (x)
-        in ``--list`` output.
-
         Item flags
         ++++++++++
 
@@ -2925,7 +3127,7 @@ def define_archive_filters_group(subparser, *, sort_by=True, first_last=True):
                                help='only print file/directory names, nothing else')
         subparser.add_argument('--format', '--list-format', metavar='FORMAT', dest='format',
                                help='specify format for file listing '
-                                    '(default: "{mode} {user:6} {group:6} {size:8d} {mtime} {path}{extra}{NL}")')
+                                    '(default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}")')
         subparser.add_argument('--json', action='store_true',
                                help='Only valid for listing repository contents. Format output as JSON. '
                                     'The form of ``--format`` is ignored, '
diff --git a/src/borg/cache.py b/src/borg/cache.py
index a9e7be27..c1c70332 100644
--- a/src/borg/cache.py
+++ b/src/borg/cache.py
@@ -503,16 +503,10 @@ def _read_files(self):
                 if not data:
                     break
                 u.feed(data)
-                try:
-                    for path_hash, item in u:
-                        entry = FileCacheEntry(*item)
-                        # in the end, this takes about 240 Bytes per file
-                        self.files[path_hash] = msgpack.packb(entry._replace(age=entry.age + 1))
-                except (TypeError, ValueError) as exc:
-                    logger.warning('The files cache seems corrupt, ignoring it. '
-                                   'Expect lower performance. [%s]' % str(exc))
-                    self.files = {}
-                    return
+                for path_hash, item in u:
+                    entry = FileCacheEntry(*item)
+                    # in the end, this takes about 240 Bytes per file
+                    self.files[path_hash] = msgpack.packb(entry._replace(age=entry.age + 1))
 
     def begin_txn(self):
         # Initialize transaction snapshot
@@ -982,7 +976,7 @@ def memorize_file(self, path_hash, st, ids):
     def add_chunk(self, id, chunk, stats, overwrite=False, wait=True):
         assert not overwrite, 'AdHocCache does not permit overwrites — trying to use it for recreate?'
         if not self._txn_active:
-            self.begin_txn()
+            self._begin_txn()
         size = len(chunk)
         refcount = self.seen_chunk(id, size)
         if refcount:
@@ -996,7 +990,7 @@ def add_chunk(self, id, chunk, stats, overwrite=False, wait=True):
 
     def seen_chunk(self, id, size=None):
         if not self._txn_active:
-            self.begin_txn()
+            self._begin_txn()
         entry = self.chunks.get(id, ChunkIndexEntry(0, None, None))
         if entry.refcount and size and not entry.size:
             # The LocalCache has existing size information and uses *size* to make an effort at detecting collisions.
@@ -1007,7 +1001,7 @@ def seen_chunk(self, id, size=None):
 
     def chunk_incref(self, id, stats, size=None):
         if not self._txn_active:
-            self.begin_txn()
+            self._begin_txn()
         count, _size, csize = self.chunks.incref(id)
         # When _size is 0 and size is not given, then this chunk has not been locally visited yet (seen_chunk with
         # size or add_chunk); we can't add references to those (size=0 is invalid) and generally don't try to.
@@ -1018,7 +1012,7 @@ def chunk_incref(self, id, stats, size=None):
 
     def chunk_decref(self, id, stats, wait=True):
         if not self._txn_active:
-            self.begin_txn()
+            self._begin_txn()
         count, size, csize = self.chunks.decref(id)
         if count == 0:
             del self.chunks[id]
@@ -1037,7 +1031,9 @@ def rollback(self):
         self._txn_active = False
         del self.chunks
 
-    def begin_txn(self):
+    # Private API
+
+    def _begin_txn(self):
         self._txn_active = True
         # Explicitly set the initial hash table capacity to avoid performance issues
         # due to hash table "resonance".
diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py
index 25114c46..c8e65295 100644
--- a/src/borg/helpers/parseformat.py
+++ b/src/borg/helpers/parseformat.py
@@ -18,7 +18,7 @@
 
 from .errors import Error
 from .fs import get_keys_dir
-from .time import OutputTimestamp, format_time, to_localtime, safe_timestamp, safe_s
+from .time import format_time, isoformat_time, to_localtime, safe_timestamp, safe_s
 from .usergroup import uid2user
 from .. import __version__ as borg_version
 from .. import __version_tuple__ as borg_version_tuple
@@ -549,11 +549,12 @@ def __init__(self, format, repository, manifest, key, *, json=False):
         if self.json:
             self.item_data = {}
             self.format_item = self.format_item_json
+            self.format_time = self.format_time_json
         else:
             self.item_data = static_keys
 
     def format_item_json(self, item):
-        return json.dumps(self.get_item_data(item), cls=BorgJsonEncoder) + '\n'
+        return json.dumps(self.get_item_data(item)) + '\n'
 
     def get_item_data(self, archive_info):
         self.name = archive_info.name
@@ -587,7 +588,12 @@ def get_ts_end(self):
         return self.format_time(self.archive.ts_end)
 
     def format_time(self, ts):
-        return OutputTimestamp(ts)
+        t = to_localtime(ts)
+        return format_time(t)
+
+    def format_time_json(self, ts):
+        t = to_localtime(ts)
+        return isoformat_time(t)
 
 
 class ItemFormatter(BaseFormatter):
@@ -663,6 +669,7 @@ def __init__(self, archive, format, *, json_lines=False):
         if self.json_lines:
             self.item_data = {}
             self.format_item = self.format_item_json
+            self.format_time = self.format_time_json
         else:
             self.item_data = static_keys
         self.format = partial_format(format, static_keys)
@@ -674,19 +681,19 @@ def __init__(self, archive, format, *, json_lines=False):
             'dcsize': partial(self.sum_unique_chunks_metadata, lambda chunk: chunk.csize),
             'num_chunks': self.calculate_num_chunks,
             'unique_chunks': partial(self.sum_unique_chunks_metadata, lambda chunk: 1),
-            'isomtime': partial(self.format_iso_time, 'mtime'),
-            'isoctime': partial(self.format_iso_time, 'ctime'),
-            'isoatime': partial(self.format_iso_time, 'atime'),
-            'mtime': partial(self.format_time, 'mtime'),
-            'ctime': partial(self.format_time, 'ctime'),
-            'atime': partial(self.format_time, 'atime'),
+            'isomtime': partial(self.format_time, 'mtime'),
+            'isoctime': partial(self.format_time, 'ctime'),
+            'isoatime': partial(self.format_time, 'atime'),
+            'mtime': partial(self.time, 'mtime'),
+            'ctime': partial(self.time, 'ctime'),
+            'atime': partial(self.time, 'atime'),
         }
         for hash_function in hashlib.algorithms_guaranteed:
             self.add_key(hash_function, partial(self.hash_item, hash_function))
         self.used_call_keys = set(self.call_keys) & self.format_keys
 
     def format_item_json(self, item):
-        return json.dumps(self.get_item_data(item), cls=BorgJsonEncoder) + '\n'
+        return json.dumps(self.get_item_data(item)) + '\n'
 
     def add_key(self, key, callable_with_item):
         self.call_keys[key] = callable_with_item
@@ -761,10 +768,15 @@ def hash_item(self, hash_function, item):
         return hash.hexdigest()
 
     def format_time(self, key, item):
-        return OutputTimestamp(safe_timestamp(item.get(key) or item.mtime))
+        t = self.time(key, item)
+        return format_time(t)
+
+    def format_time_json(self, key, item):
+        t = self.time(key, item)
+        return isoformat_time(t)
 
-    def format_iso_time(self, key, item):
-        return self.format_time(key, item).isoformat()
+    def time(self, key, item):
+        return safe_timestamp(item.get(key) or item.mtime)
 
 
 def file_status(mode):
@@ -875,8 +887,6 @@ def default(self, o):
             return {
                 'stats': o.stats(),
             }
-        if callable(getattr(o, 'to_json', None)):
-            return o.to_json()
         return super().default(o)
 
 
@@ -889,7 +899,7 @@ def basic_json_data(manifest, *, cache=None, extra=None):
             'mode': key.ARG_NAME,
         },
     })
-    data['repository']['last_modified'] = OutputTimestamp(manifest.last_timestamp.replace(tzinfo=timezone.utc))
+    data['repository']['last_modified'] = isoformat_time(to_localtime(manifest.last_timestamp.replace(tzinfo=timezone.utc)))
     if key.NAME.startswith('key file'):
         data['encryption']['keyfile'] = key.find_key()
     if cache:
diff --git a/src/borg/helpers/time.py b/src/borg/helpers/time.py
index 2d22794a..7203ba2f 100644
--- a/src/borg/helpers/time.py
+++ b/src/borg/helpers/time.py
@@ -86,19 +86,16 @@ def safe_timestamp(item_timestamp_ns):
     return datetime.fromtimestamp(t_ns / 1e9)
 
 
-def format_time(ts: datetime):
+def format_time(t):
+    """use ISO-8601-like date and time format (human readable, with wkday and blank date/time separator)
     """
-    Convert *ts* to a human-friendly format with textual weekday.
-    """
-    return ts.strftime('%a, %Y-%m-%d %H:%M:%S')
+    return t.strftime('%a, %Y-%m-%d %H:%M:%S')
 
 
-def isoformat_time(ts: datetime):
-    """
-    Format *ts* according to ISO 8601.
+def isoformat_time(t):
+    """use ISO-8601 date and time format (machine readable, no wkday, no microseconds either)
     """
-    # note: first make all datetime objects tz aware before adding %z here.
-    return ts.strftime('%Y-%m-%dT%H:%M:%S.%f')
+    return t.strftime('%Y-%m-%dT%H:%M:%S')  # note: first make all datetime objects tz aware before adding %z here.
 
 
 def format_timedelta(td):
@@ -116,21 +113,3 @@ def format_timedelta(td):
     if td.days:
         txt = '%d days %s' % (td.days, txt)
     return txt
-
-
-class OutputTimestamp:
-    def __init__(self, ts: datetime):
-        if ts.tzinfo == timezone.utc:
-            ts = to_localtime(ts)
-        self.ts = ts
-
-    def __format__(self, format_spec):
-        return format_time(self.ts)
-
-    def __str__(self):
-        return '{}'.format(self)
-
-    def isoformat(self):
-        return isoformat_time(self.ts)
-
-    to_json = isoformat
diff --git a/src/borg/item.pyx b/src/borg/item.pyx
index a8a21c04..f8b67ddd 100644
--- a/src/borg/item.pyx
+++ b/src/borg/item.pyx
@@ -5,7 +5,6 @@ from .constants import ITEM_KEYS
 from .helpers import safe_encode, safe_decode
 from .helpers import bigint_to_int, int_to_bigint
 from .helpers import StableDict
-from .helpers import format_file_size
 
 cdef extern from "_item.c":
     object _object_to_optr(object obj)
@@ -185,22 +184,19 @@ class Item(PropDict):
 
     part = PropDict._make_property('part', int)
 
-    def get_size(self, hardlink_masters=None, memorize=False, compressed=False, from_chunks=False, consider_ids=None):
+    def get_size(self, hardlink_masters=None, memorize=False, compressed=False, from_chunks=False):
         """
         Determine the (uncompressed or compressed) size of this item.
 
-        :param hardlink_masters: If given, the size of hardlink slaves is computed via the hardlink master's chunk list,
-        otherwise size will be returned as 0.
-        :param memorize: Whether the computed size value will be stored into the item.
-        :param compressed: Whether the compressed or uncompressed size will be returned.
-        :param from_chunks: If true, size is computed from chunks even if a precomputed value is available.
-        :param consider_ids: Returns the size of the given ids only.
+        For hardlink slaves, the size is computed via the hardlink master's
+        chunk list, if available (otherwise size will be returned as 0).
+
+        If memorize is True, the computed size value will be stored into the item.
         """
         attr = 'csize' if compressed else 'size'
         assert not (compressed and memorize), 'Item does not have a csize field.'
-        assert not (consider_ids is not None and memorize), "Can't store size when considering only certain ids"
         try:
-            if from_chunks or consider_ids is not None:
+            if from_chunks:
                 raise AttributeError
             size = getattr(self, attr)
         except AttributeError:
@@ -230,10 +226,7 @@ class Item(PropDict):
                         chunks, _ = hardlink_masters.get(master, (None, None))
                 if chunks is None:
                     return 0
-            if consider_ids is not None:
-                size = sum(getattr(ChunkListEntry(*chunk), attr) for chunk in chunks if chunk.id in consider_ids)
-            else:
-                size = sum(getattr(ChunkListEntry(*chunk), attr) for chunk in chunks)
+            size = sum(getattr(ChunkListEntry(*chunk), attr) for chunk in chunks)
             # if requested, memorize the precomputed (c)size for items that have an own chunks list:
             if memorize and having_chunks:
                 setattr(self, attr, size)
@@ -258,21 +251,6 @@ class Item(PropDict):
     def from_optr(self, optr):
         return _optr_to_object(optr)
 
-    @classmethod
-    def create_deleted(cls, path):
-        return cls(deleted=True, chunks=[], mode=0, path=path)
-
-    def is_link(self):
-        return self._is_type(stat.S_ISLNK)
-
-    def is_dir(self):
-        return self._is_type(stat.S_ISDIR)
-
-    def _is_type(self, typetest):
-        try:
-            return typetest(self.mode)
-        except AttributeError:
-            return False
 
 
 class EncryptedKey(PropDict):
@@ -380,140 +358,3 @@ class ManifestItem(PropDict):
     timestamp = PropDict._make_property('timestamp', str, 'surrogate-escaped str', encode=safe_encode, decode=safe_decode)
     config = PropDict._make_property('config', dict)
     item_keys = PropDict._make_property('item_keys', tuple)
-
-class ItemDiff:
-    """
-    Comparison of two items from different archives.
-
-    The items may have different paths and still be considered equal (e.g. for renames).
-    It does not include extended or time attributes in the comparison.
-    """
-
-    def __init__(self, item1, item2, chunk_iterator1, chunk_iterator2, numeric_owner=False, can_compare_chunk_ids=False):
-        self._item1 = item1
-        self._item2 = item2
-        self._numeric_owner = numeric_owner
-        self._can_compare_chunk_ids = can_compare_chunk_ids
-        self.equal = self._equal(chunk_iterator1, chunk_iterator2)
-
-    def __repr__(self):
-        if self.equal:
-            return 'equal'
-
-        changes = []
-
-        if self._item1.is_link() or self._item2.is_link():
-            changes.append(self._link_string())
-
-        if 'chunks' in self._item1 and 'chunks' in self._item2:
-            changes.append(self._content_string())
-
-        if self._item1.is_dir() or self._item2.is_dir():
-            changes.append(self._dir_string())
-
-        if not (self._item1.get('deleted') or self._item2.get('deleted')):
-            changes.append(self._owner_string())
-            changes.append(self._mode_string())
-
-        return ' '.join((x for x in changes if x))
-
-    def _equal(self, chunk_iterator1, chunk_iterator2):
-        # if both are deleted, there is nothing at path regardless of what was deleted
-        if self._item1.get('deleted') and self._item2.get('deleted'):
-            return True
-
-        attr_list = ['deleted', 'mode', 'source']
-        attr_list += ['uid', 'gid'] if self._numeric_owner else ['user', 'group']
-        for attr in attr_list:
-            if self._item1.get(attr) != self._item2.get(attr):
-                return False
-
-        if 'mode' in self._item1:     # mode of item1 and item2 is equal
-            if (self._item1.is_link() and 'source' in self._item1 and 'source' in self._item2
-                and self._item1.source != self._item2.source):
-                return False
-
-        if 'chunks' in self._item1 and 'chunks' in self._item2:
-            return self._content_equal(chunk_iterator1, chunk_iterator2)
-
-        return True
-
-    def _link_string(self):
-        if self._item1.get('deleted'):
-            return 'added link'
-        if self._item2.get('deleted'):
-            return 'removed link'
-        if 'source' in self._item1 and 'source' in self._item2 and self._item1.source != self._item2.source:
-            return 'changed link'
-
-    def _content_string(self):
-        if self._item1.get('deleted'):
-            return ('added {:>13}'.format(format_file_size(self._item2.get_size())))
-        if self._item2.get('deleted'):
-            return ('removed {:>11}'.format(format_file_size(self._item1.get_size())))
-        if not self._can_compare_chunk_ids:
-            return 'modified'
-        chunk_ids1 = {c.id for c in self._item1.chunks}
-        chunk_ids2 = {c.id for c in self._item2.chunks}
-        added_ids = chunk_ids2 - chunk_ids1
-        removed_ids = chunk_ids1 - chunk_ids2
-        added = self._item2.get_size(consider_ids=added_ids)
-        removed = self._item1.get_size(consider_ids=removed_ids)
-        return ('{:>9} {:>9}'.format(format_file_size(added, precision=1, sign=True),
-                                     format_file_size(-removed, precision=1, sign=True)))
-
-    def _dir_string(self):
-        if self._item2.get('deleted') and not self._item1.get('deleted'):
-            return 'removed directory'
-        if self._item1.get('deleted') and not self._item2.get('deleted'):
-            return 'added directory'
-
-    def _owner_string(self):
-        u_attr, g_attr = ('uid', 'gid') if self._numeric_owner else ('user', 'group')
-        u1, g1 = self._item1.get(u_attr), self._item1.get(g_attr)
-        u2, g2 = self._item2.get(u_attr), self._item2.get(g_attr)
-        if (u1, g1) != (u2, g2):
-            return '[{}:{} -> {}:{}]'.format(u1, g1, u2, g2)
-
-    def _mode_string(self):
-        if 'mode' in self._item1 and 'mode' in self._item2 and self._item1.mode != self._item2.mode:
-            return '[{} -> {}]'.format(stat.filemode(self._item1.mode), stat.filemode(self._item2.mode))
-
-    def _content_equal(self, chunk_iterator1, chunk_iterator2):
-        if self._can_compare_chunk_ids:
-            return self._item1.chunks == self._item2.chunks
-        if self._item1.get_size() != self._item2.get_size():
-            return False
-        return ItemDiff._chunk_content_equal(chunk_iterator1, chunk_iterator2)
-
-    @staticmethod
-    def _chunk_content_equal(chunks1, chunks2):
-        """
-        Compare chunk content and return True if they are identical.
-
-        The chunks must be given as chunk iterators (like returned by :meth:`.DownloadPipeline.fetch_many`).
-        """
-
-        end = object()
-        alen = ai = 0
-        blen = bi = 0
-        while True:
-            if not alen - ai:
-                a = next(chunks1, end)
-                if a is end:
-                    return not blen - bi and next(chunks2, end) is end
-                a = memoryview(a)
-                alen = len(a)
-                ai = 0
-            if not blen - bi:
-                b = next(chunks2, end)
-                if b is end:
-                    return not alen - ai and next(chunks1, end) is end
-                b = memoryview(b)
-                blen = len(b)
-                bi = 0
-            slicelen = min(alen - ai, blen - bi)
-            if a[ai:ai + slicelen] != b[bi:bi + slicelen]:
-                return False
-            ai += slicelen
-            bi += slicelen


Test patch:
diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py
index 947612a1..4de155be 100644
--- a/src/borg/testsuite/archiver.py
+++ b/src/borg/testsuite/archiver.py
@@ -47,7 +47,7 @@
 from ..helpers import MAX_S
 from ..nanorst import RstToTextLazy, rst_to_terminal
 from ..patterns import IECommand, PatternMatcher, parse_pattern
-from ..item import Item, ItemDiff
+from ..item import Item
 from ..logger import setup_logging
 from ..remote import RemoteRepository, PathNotAllowed
 from ..repository import Repository
@@ -61,7 +61,7 @@
 
 src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
 
-ISO_FORMAT = '%Y-%m-%dT%H:%M:%S.%f'
+ISO_FORMAT = '%Y-%m-%dT%H:%M:%S'
 
 
 def exec_cmd(*args, archiver=None, fork=False, exe=None, input=b'', binary_output=False, **kw):
@@ -1326,8 +1326,6 @@ def test_info_json(self):
         assert isinstance(archive['duration'], float)
         assert len(archive['id']) == 64
         assert 'stats' in archive
-        assert datetime.strptime(archive['start'], ISO_FORMAT)
-        assert datetime.strptime(archive['end'], ISO_FORMAT)
 
     def test_comment(self):
         self.create_regular_file('file1', size=1024 * 80)
@@ -1790,7 +1788,7 @@ def test_list_format(self):
         output_warn = self.cmd('list', '--list-format', '-', test_archive)
         self.assert_in('--list-format" has been deprecated.', output_warn)
         output_1 = self.cmd('list', test_archive)
-        output_2 = self.cmd('list', '--format', '{mode} {user:6} {group:6} {size:8d} {mtime} {path}{extra}{NEWLINE}', test_archive)
+        output_2 = self.cmd('list', '--format', '{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NEWLINE}', test_archive)
         output_3 = self.cmd('list', '--format', '{mtime:%s} {path}{NL}', test_archive)
         self.assertEqual(output_1, output_2)
         self.assertNotEqual(output_1, output_3)
@@ -1864,7 +1862,7 @@ def test_list_json(self):
         file1 = items[1]
         assert file1['path'] == 'input/file1'
         assert file1['size'] == 81920
-        assert datetime.strptime(file1['mtime'], ISO_FORMAT)  # must not raise
+        assert datetime.strptime(file1['isomtime'], ISO_FORMAT)  # must not raise
 
         list_archive = self.cmd('list', '--json-lines', '--format={sha256}', self.repository_location + '::test')
         items = [json.loads(s) for s in list_archive.splitlines()]
@@ -3432,12 +3430,12 @@ def test_get_args():
     assert args.func == archiver.do_serve
 
 
-def test_chunk_content_equal():
+def test_compare_chunk_contents():
     def ccc(a, b):
         chunks_a = [data for data in a]
         chunks_b = [data for data in b]
-        compare1 = ItemDiff._chunk_content_equal(iter(chunks_a), iter(chunks_b))
-        compare2 = ItemDiff._chunk_content_equal(iter(chunks_b), iter(chunks_a))
+        compare1 = Archiver.compare_chunk_contents(iter(chunks_a), iter(chunks_b))
+        compare2 = Archiver.compare_chunk_contents(iter(chunks_b), iter(chunks_a))
         assert compare1 == compare2
         return compare1
     assert ccc([