codecov-io: # [Codecov](https://codecov.io/gh/borgbackup/borg/pull/3024?src=pr&el=h1) Report
> Merging [#3024](https://codecov.io/gh/borgbackup/borg/pull/3024?src=pr&el=desc) into [master](https://codecov.io/gh/borgbackup/borg/commit/7c5a9d89b2a8deac84a641bb4ccad00a575a2765?src=pr&el=desc) will **decrease** coverage by `0.83%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/borgbackup/borg/pull/3024/graphs/tree.svg?width=650&src=pr&token=LssEpmW3o4&height=150)](https://codecov.io/gh/borgbackup/borg/pull/3024?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #3024      +/-   ##
==========================================
- Coverage      86%   85.16%   -0.84%     
==========================================
  Files          36       36              
  Lines        8891     8894       +3     
  Branches     1468     1468              
==========================================
- Hits         7647     7575      -72     
- Misses        844      914      +70     
- Partials      400      405       +5
```


| [Impacted Files](https://codecov.io/gh/borgbackup/borg/pull/3024?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/borg/archive.py](https://codecov.io/gh/borgbackup/borg/pull/3024?src=pr&el=tree#diff-c3JjL2JvcmcvYXJjaGl2ZS5weQ==) | `83.1% <100%> (-0.83%)` | :arrow_down: |
| [src/borg/cache.py](https://codecov.io/gh/borgbackup/borg/pull/3024?src=pr&el=tree#diff-c3JjL2JvcmcvY2FjaGUucHk=) | `86.53% <100%> (+0.01%)` | :arrow_up: |
| [src/borg/archiver.py](https://codecov.io/gh/borgbackup/borg/pull/3024?src=pr&el=tree#diff-c3JjL2JvcmcvYXJjaGl2ZXIucHk=) | `86.64% <100%> (+0.01%)` | :arrow_up: |
| [src/borg/xattr.py](https://codecov.io/gh/borgbackup/borg/pull/3024?src=pr&el=tree#diff-c3JjL2JvcmcveGF0dHIucHk=) | `59.5% <0%> (-23.5%)` | :arrow_down: |
| [src/borg/platform/\_\_init\_\_.py](https://codecov.io/gh/borgbackup/borg/pull/3024?src=pr&el=tree#diff-c3JjL2JvcmcvcGxhdGZvcm0vX19pbml0X18ucHk=) | `73.68% <0%> (-15.79%)` | :arrow_down: |
| [src/borg/platform/base.py](https://codecov.io/gh/borgbackup/borg/pull/3024?src=pr&el=tree#diff-c3JjL2JvcmcvcGxhdGZvcm0vYmFzZS5weQ==) | `75.9% <0%> (-10.85%)` | :arrow_down: |
| [src/borg/helpers/usergroup.py](https://codecov.io/gh/borgbackup/borg/pull/3024?src=pr&el=tree#diff-c3JjL2JvcmcvaGVscGVycy91c2VyZ3JvdXAucHk=) | `59.45% <0%> (-5.41%)` | :arrow_down: |
| [src/borg/helpers/fs.py](https://codecov.io/gh/borgbackup/borg/pull/3024?src=pr&el=tree#diff-c3JjL2JvcmcvaGVscGVycy9mcy5weQ==) | `93.18% <0%> (-2.28%)` | :arrow_down: |
| [src/borg/helpers/misc.py](https://codecov.io/gh/borgbackup/borg/pull/3024?src=pr&el=tree#diff-c3JjL2JvcmcvaGVscGVycy9taXNjLnB5) | `80.86% <0%> (-0.87%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/borgbackup/borg/pull/3024?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/borgbackup/borg/pull/3024?src=pr&el=footer). Last update [7c5a9d8...f104f47](https://codecov.io/gh/borgbackup/borg/pull/3024?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

ThomasWaldmann: any feedback?
enkore: I don't like extra options for such obscure details. I think it would be better to conclude either possibility and have no new option.
ThomasWaldmann: I don't like them either, but there is no clear choice here.

A lot of tools just use mtime and mtime is faster.

ctime is safer because userspace software can't do weird stuff with it, but slower if someone does a chmod/chown/ln over bigger amounts of data.

So the rationale of this PR is "do it safe by default, leave option to do it quick (if admin can assert no strange stuff is going on with mtime)".
ThomasWaldmann: about using `max(mtime, ctime)` instead of just `ctime`:

good:
- if a fs would return a completely broken ctime (like always 0), our cache would use mtime and still work

bad:
- more complex behaviour

ThomasWaldmann: I'm going to merge this soon as I don't see any better alternative.

Whether using max(mtime, ctime) is any good / is really needed is still an open question.

Guess if we supported native windows, using max() would be good as there ctime is creation time (not change time), thus not helpful for what we need.
enkore: I think max(mtime, ctime) is kinda like the worst combination of both. It still re-chunks on metadata-only changes, but doesn't have the idiot-safety of ctime.
ThomasWaldmann:  Hm? It would keep the safety of ctime IF ctime is supported by the OS / fs in a UNIX-like way.

The problem with mtime is that it can be faked to stay same, even though the file was changed.
ctime would change to a bigger numerical value and can't be faked, so max(ctime, mtime) would result in the ctime for such a case.

The only cases when max(ctime, mtime) result in mtime is when ctime does not work at all (like always 0, not sure if that can be seen out there) or when ctime means creation time and thus is always smaller than mtime.

enkore: Ah right.
enkore: Right, but I could e.g. change the mtime to some very large value (mtime > ctime), modify the file, re-set mtime to the large value etc. 
ThomasWaldmann: Yup, that would escape the change detection if there was a backup after the first far-future mtime change, but before the file modification. Crap.
ThomasWaldmann: ctime << 64 + mtime?
ThomasWaldmann: Note: i kept it as a cmdline option rather than env var.

We have --ignore-inode and --no-files-cache now, but they can be superceded by just using the respective --cache-mode options. So, 1 option less long term than before this changeset.

For local UNIX file systems and when valuing safety over speed, no option must be used.
If ctime or inode numbers are not working as expected, options can be used to work around that.

Using env vars for this feels somehow more clunky, esp. when the source filesystems need different cache mode values.
ThomasWaldmann: src/borg/archiver.py:2614:1: W293 blank line contains whitespace

https://travis-ci.org/borgbackup/borg/jobs/280267622

There is no whitespace.
enkore: Ok. I think it would be better to use less cryptic option names, consider `--cache-mode=ignore-inode,mtime`.
ThomasWaldmann: @enkore can you do a final review before I merge this?
diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst
index 045b476b..7c6d46b6 100644
--- a/docs/internals/data-structures.rst
+++ b/docs/internals/data-structures.rst
@@ -738,7 +738,7 @@ b) with ``create --chunker-params 19,23,21,4095`` (default):
 
   mem_usage  =  0.31GiB
 
-.. note:: There is also the ``--no-files-cache`` option to switch off the files cache.
+.. note:: There is also the ``--files-cache=disabled`` option to disable the files cache.
    You'll save some memory, but it will need to read / chunk all the files as
    it can not skip unmodified files then.
 
diff --git a/src/borg/archive.py b/src/borg/archive.py
index 3cb0b7cf..9bb2b8b1 100644
--- a/src/borg/archive.py
+++ b/src/borg/archive.py
@@ -1087,13 +1087,13 @@ def process_stdin(self, path, cache):
         self.add_item(item)
         return 'i'  # stdin
 
-    def process_file(self, path, st, cache, ignore_inode=False):
+    def process_file(self, path, st, cache, ignore_inode=False, files_cache_mode=DEFAULT_FILES_CACHE_MODE):
         with self.create_helper(path, st, None) as (item, status, hardlinked, hardlink_master):  # no status yet
             is_special_file = is_special(st.st_mode)
             if not hardlinked or hardlink_master:
                 if not is_special_file:
                     path_hash = self.key.id_hash(safe_encode(os.path.join(self.cwd, path)))
-                    ids = cache.file_known_and_unchanged(path_hash, st, ignore_inode)
+                    ids = cache.file_known_and_unchanged(path_hash, st, ignore_inode, files_cache_mode)
                 else:
                     # in --read-special mode, we may be called for special files.
                     # there should be no information in the cache about special files processed in
@@ -1126,7 +1126,7 @@ def process_file(self, path, st, cache, ignore_inode=False):
                     if not is_special_file:
                         # we must not memorize special files, because the contents of e.g. a
                         # block or char device will change without its mtime/size/inode changing.
-                        cache.memorize_file(path_hash, st, [c.id for c in item.chunks])
+                        cache.memorize_file(path_hash, st, [c.id for c in item.chunks], files_cache_mode)
                     status = status or 'M'  # regular file, modified (if not 'A' already)
                 self.stats.nfiles += 1
             item.update(self.metadata_collector.stat_attrs(st, path))
diff --git a/src/borg/archiver.py b/src/borg/archiver.py
index 394d802b..039587dd 100644
--- a/src/borg/archiver.py
+++ b/src/borg/archiver.py
@@ -45,7 +45,7 @@
 from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR
 from .helpers import Error, NoManifestError, set_ec
 from .helpers import positive_int_validator, location_validator, archivename_validator, ChunkerParams
-from .helpers import PrefixSpec, SortBySpec
+from .helpers import PrefixSpec, SortBySpec, FilesCacheMode
 from .helpers import BaseFormatter, ItemFormatter, ArchiveFormatter
 from .helpers import format_timedelta, format_file_size, parse_file_size, format_archive
 from .helpers import safe_encode, remove_surrogates, bin_to_hex, prepare_dump_dict
@@ -346,7 +346,7 @@ def measurement_run(repo, path):
             compression = '--compression=none'
             # measure create perf (without files cache to always have it chunking)
             t_start = time.monotonic()
-            rc = self.do_create(self.parse_args(['create', compression, '--no-files-cache', archive + '1', path]))
+            rc = self.do_create(self.parse_args(['create', compression, '--files-cache=disabled', archive + '1', path]))
             t_end = time.monotonic()
             dt_create = t_end - t_start
             assert rc == 0
@@ -485,6 +485,7 @@ def create_inner(archive, cache, fso):
         self.output_filter = args.output_filter
         self.output_list = args.output_list
         self.ignore_inode = args.ignore_inode
+        self.files_cache_mode = args.files_cache_mode
         dry_run = args.dry_run
         t0 = datetime.utcnow()
         t0_monotonic = time.monotonic()
@@ -548,7 +549,7 @@ def _process(self, fso, cache, matcher, exclude_caches, exclude_if_present,
                     return
             if stat.S_ISREG(st.st_mode):
                 if not dry_run:
-                    status = fso.process_file(path, st, cache, self.ignore_inode)
+                    status = fso.process_file(path, st, cache, self.ignore_inode, self.files_cache_mode)
             elif stat.S_ISDIR(st.st_mode):
                 if recurse:
                     tag_paths = dir_is_tagged(path, exclude_caches, exclude_if_present)
@@ -1960,14 +1961,17 @@ def do_subcommand_help(self, parser, args):
 
     def preprocess_args(self, args):
         deprecations = [
-            # ('--old', '--new', 'Warning: "--old" has been deprecated. Use "--new" instead.'),
+            # ('--old', '--new' or None, 'Warning: "--old" has been deprecated. Use "--new" instead.'),
             ('--list-format', '--format', 'Warning: "--list-format" has been deprecated. Use "--format" instead.'),
             ('--keep-tag-files', '--keep-exclude-tags', 'Warning: "--keep-tag-files" has been deprecated. Use "--keep-exclude-tags" instead.'),
+            ('--ignore-inode', None, 'Warning: "--ignore-inode" has been deprecated. Use "--files-cache=ctime,size" or "...=mtime,size" instead.'),
+            ('--no-files-cache', None, 'Warning: "--no-files-cache" has been deprecated. Use "--files-cache=disabled" instead.'),
         ]
         for i, arg in enumerate(args[:]):
             for old_name, new_name, warning in deprecations:
                 if arg.startswith(old_name):
-                    args[i] = arg.replace(old_name, new_name)
+                    if new_name is not None:
+                        args[i] = arg.replace(old_name, new_name)
                     print(warning, file=sys.stderr)
         return args
 
@@ -2595,13 +2599,39 @@ def define_archive_filters_group(subparser, *, sort_by=True, first_last=True):
         {now}, {utcnow}, {fqdn}, {hostname}, {user} and some others.
 
         Backup speed is increased by not reprocessing files that are already part of
-        existing archives and weren't modified. Normally, detecting file modifications
-        will take inode information into consideration. This is problematic for files
-        located on sshfs and similar network file systems which do not provide stable
-        inode numbers, such files will always be considered modified. The
-        ``--ignore-inode`` flag can be used to prevent this and improve performance.
-        This flag will reduce reliability of change detection however, with files
-        considered unmodified as long as their size and modification time are unchanged.
+        existing archives and weren't modified. The detection of unmodified files is
+        done by comparing multiple file metadata values with previous values kept in
+        the files cache.
+
+        This comparison can operate in different modes as given by ``--files-cache``:
+
+        - ctime,size,inode (default)
+        - mtime,size,inode (default behaviour of borg versions older than 1.1.0rc4)
+        - ctime,size (ignore the inode number)
+        - mtime,size (ignore the inode number)
+        - rechunk,ctime (all files are considered modified - rechunk, cache ctime)
+        - rechunk,mtime (all files are considered modified - rechunk, cache mtime)
+        - disabled (disable the files cache, all files considered modified - rechunk)
+
+        inode number: better safety, but often unstable on network filesystems
+
+        Normally, detecting file modifications will take inode information into
+        consideration to improve the reliability of file change detection.
+        This is problematic for files located on sshfs and similar network file
+        systems which do not provide stable inode numbers, such files will always
+        be considered modified. You can use modes without `inode` in this case to
+        improve performance, but reliability of change detection might be reduced.
+
+        ctime vs. mtime: safety vs. speed
+
+        - ctime is a rather safe way to detect changes to a file (metadata and contents)
+          as it can not be set from userspace. But, a metadata-only change will already
+          update the ctime, so there might be some unnecessary chunking/hashing even
+          without content changes. Some filesystems do not support ctime (change time).
+        - mtime usually works and only updates if file contents were changed. But mtime
+          can be arbitrarily set from userspace, e.g. to set mtime back to the same value
+          it had before a content change happened. This can be used maliciously as well as
+          well-meant, but in both cases mtime based cache modes can be problematic.
 
         The mount points of filesystems or filesystem snapshots should be the same for every
         creation of a new archive to ensure fast operation. This is because the file cache that
@@ -2692,7 +2722,7 @@ def define_archive_filters_group(subparser, *, sort_by=True, first_last=True):
         subparser.add_argument('--json', action='store_true',
                                help='output stats as JSON. Implies ``--stats``.')
         subparser.add_argument('--no-cache-sync', dest='no_cache_sync', action='store_true',
-                               help='experimental: do not synchronize the cache. Implies ``--no-files-cache``.')
+                               help='experimental: do not synchronize the cache. Implies not using the files cache.')
 
         define_exclusion_group(subparser, tag_files=True)
 
@@ -2707,6 +2737,9 @@ def define_archive_filters_group(subparser, *, sort_by=True, first_last=True):
                               help='do not store ctime into archive')
         fs_group.add_argument('--ignore-inode', dest='ignore_inode', action='store_true',
                               help='ignore inode data in the file metadata cache used to detect unchanged files.')
+        fs_group.add_argument('--files-cache', metavar='MODE', dest='files_cache_mode',
+                              type=FilesCacheMode, default=DEFAULT_FILES_CACHE_MODE_UI,
+                              help='operate files cache in MODE. default: %s' % DEFAULT_FILES_CACHE_MODE_UI)
         fs_group.add_argument('--read-special', dest='read_special', action='store_true',
                               help='open and read block and char device files as well as FIFOs as if they were '
                                    'regular files. Also follows symlinks pointing to these kinds of files.')
diff --git a/src/borg/cache.py b/src/borg/cache.py
index a9e7be27..227858bc 100644
--- a/src/borg/cache.py
+++ b/src/borg/cache.py
@@ -12,7 +12,7 @@
 
 logger = create_logger()
 
-from .constants import CACHE_README
+from .constants import CACHE_README, DEFAULT_FILES_CACHE_MODE
 from .hashindex import ChunkIndex, ChunkIndexEntry, CacheSynchronizer
 from .helpers import Location
 from .helpers import Error
@@ -34,7 +34,8 @@
 from .remote import cache_if_remote
 from .repository import LIST_SCAN_LIMIT
 
-FileCacheEntry = namedtuple('FileCacheEntry', 'age inode size mtime chunk_ids')
+# note: cmtime might me either a ctime or a mtime timestamp
+FileCacheEntry = namedtuple('FileCacheEntry', 'age inode size cmtime chunk_ids')
 
 
 class SecurityManager:
@@ -492,7 +493,7 @@ def close(self):
 
     def _read_files(self):
         self.files = {}
-        self._newest_mtime = None
+        self._newest_cmtime = None
         logger.debug('Reading files cache ...')
 
         with IntegrityCheckedFile(path=os.path.join(self.path, 'files'), write=False,
@@ -538,18 +539,18 @@ def commit(self):
         self.security_manager.save(self.manifest, self.key)
         pi = ProgressIndicatorMessage(msgid='cache.commit')
         if self.files is not None:
-            if self._newest_mtime is None:
+            if self._newest_cmtime is None:
                 # was never set because no files were modified/added
-                self._newest_mtime = 2 ** 63 - 1  # nanoseconds, good until y2262
+                self._newest_cmtime = 2 ** 63 - 1  # nanoseconds, good until y2262
             ttl = int(os.environ.get('BORG_FILES_CACHE_TTL', 20))
             pi.output('Saving files cache')
             with IntegrityCheckedFile(path=os.path.join(self.path, 'files'), write=True) as fd:
                 for path_hash, item in self.files.items():
-                    # Only keep files seen in this backup that are older than newest mtime seen in this backup -
-                    # this is to avoid issues with filesystem snapshots and mtime granularity.
+                    # Only keep files seen in this backup that are older than newest cmtime seen in this backup -
+                    # this is to avoid issues with filesystem snapshots and cmtime granularity.
                     # Also keep files from older backups that have not reached BORG_FILES_CACHE_TTL yet.
                     entry = FileCacheEntry(*msgpack.unpackb(item))
-                    if entry.age == 0 and bigint_to_int(entry.mtime) < self._newest_mtime or \
+                    if entry.age == 0 and bigint_to_int(entry.cmtime) < self._newest_cmtime or \
                        entry.age > 0 and entry.age < ttl:
                         msgpack.pack((path_hash, entry), fd)
             self.cache_config.integrity['files'] = fd.integrity_data
@@ -902,37 +903,47 @@ def chunk_decref(self, id, stats, wait=True):
         else:
             stats.update(-size, -csize, False)
 
-    def file_known_and_unchanged(self, path_hash, st, ignore_inode=False):
-        if not (self.do_files and stat.S_ISREG(st.st_mode)):
+    def file_known_and_unchanged(self, path_hash, st, ignore_inode=False, cache_mode=DEFAULT_FILES_CACHE_MODE):
+        if 'd' in cache_mode or not self.do_files or not stat.S_ISREG(st.st_mode):  # d(isabled)
             return None
         if self.files is None:
             self._read_files()
+        if 'r' in cache_mode:  # r(echunk)
+            return None
         entry = self.files.get(path_hash)
         if not entry:
             return None
         entry = FileCacheEntry(*msgpack.unpackb(entry))
-        if (entry.size == st.st_size and bigint_to_int(entry.mtime) == st.st_mtime_ns and
-                (ignore_inode or entry.inode == st.st_ino)):
-            # we ignored the inode number in the comparison above or it is still same.
-            # if it is still the same, replacing it in the tuple doesn't change it.
-            # if we ignored it, a reason for doing that is that files were moved to a new
-            # disk / new fs (so a one-time change of inode number is expected) and we wanted
-            # to avoid everything getting chunked again. to be able to re-enable the inode
-            # number comparison in a future backup run (and avoid chunking everything
-            # again at that time), we need to update the inode number in the cache with what
-            # we see in the filesystem.
-            self.files[path_hash] = msgpack.packb(entry._replace(inode=st.st_ino, age=0))
-            return entry.chunk_ids
-        else:
+        if 's' in cache_mode and entry.size != st.st_size:
             return None
-
-    def memorize_file(self, path_hash, st, ids):
-        if not (self.do_files and stat.S_ISREG(st.st_mode)):
+        if 'i' in cache_mode and not ignore_inode and entry.inode != st.st_ino:
+            return None
+        if 'c' in cache_mode and bigint_to_int(entry.cmtime) != st.st_ctime_ns:
+            return None
+        elif 'm' in cache_mode and bigint_to_int(entry.cmtime) != st.st_mtime_ns:
+            return None
+        # we ignored the inode number in the comparison above or it is still same.
+        # if it is still the same, replacing it in the tuple doesn't change it.
+        # if we ignored it, a reason for doing that is that files were moved to a new
+        # disk / new fs (so a one-time change of inode number is expected) and we wanted
+        # to avoid everything getting chunked again. to be able to re-enable the inode
+        # number comparison in a future backup run (and avoid chunking everything
+        # again at that time), we need to update the inode number in the cache with what
+        # we see in the filesystem.
+        self.files[path_hash] = msgpack.packb(entry._replace(inode=st.st_ino, age=0))
+        return entry.chunk_ids
+
+    def memorize_file(self, path_hash, st, ids, cache_mode=DEFAULT_FILES_CACHE_MODE):
+        # note: r(echunk) modes will update the files cache, d(isabled) mode won't
+        if 'd' in cache_mode or not self.do_files or not stat.S_ISREG(st.st_mode):
             return
-        mtime_ns = safe_ns(st.st_mtime_ns)
-        entry = FileCacheEntry(age=0, inode=st.st_ino, size=st.st_size, mtime=int_to_bigint(mtime_ns), chunk_ids=ids)
+        if 'c' in cache_mode:
+            cmtime_ns = safe_ns(st.st_ctime_ns)
+        elif 'm' in cache_mode:
+            cmtime_ns = safe_ns(st.st_mtime_ns)
+        entry = FileCacheEntry(age=0, inode=st.st_ino, size=st.st_size, cmtime=int_to_bigint(cmtime_ns), chunk_ids=ids)
         self.files[path_hash] = msgpack.packb(entry)
-        self._newest_mtime = max(self._newest_mtime or 0, mtime_ns)
+        self._newest_cmtime = max(self._newest_cmtime or 0, cmtime_ns)
 
 
 class AdHocCache(CacheStatsMixin):
@@ -973,10 +984,10 @@ def __exit__(self, exc_type, exc_val, exc_tb):
     files = None
     do_files = False
 
-    def file_known_and_unchanged(self, path_hash, st, ignore_inode=False):
+    def file_known_and_unchanged(self, path_hash, st, ignore_inode=False, cache_mode=DEFAULT_FILES_CACHE_MODE):
         return None
 
-    def memorize_file(self, path_hash, st, ids):
+    def memorize_file(self, path_hash, st, ids, cache_mode=DEFAULT_FILES_CACHE_MODE):
         pass
 
     def add_chunk(self, id, chunk, stats, overwrite=False, wait=True):
diff --git a/src/borg/constants.py b/src/borg/constants.py
index 36d6ee45..4f2a430d 100644
--- a/src/borg/constants.py
+++ b/src/borg/constants.py
@@ -60,6 +60,10 @@
 # chunker params for the items metadata stream, finer granularity
 ITEMS_CHUNKER_PARAMS = (15, 19, 17, HASH_WINDOW_SIZE)
 
+# operating mode of the files cache (for fast skipping of unchanged files)
+DEFAULT_FILES_CACHE_MODE_UI = 'ctime,size,inode'
+DEFAULT_FILES_CACHE_MODE = 'cis'  # == CacheMode(DEFAULT_FILES_CACHE_MODE_UI)
+
 # return codes returned by borg command
 # when borg is killed by signal N, rc = 128 + N
 EXIT_SUCCESS = 0  # everything done, no problems
diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py
index f926bbe9..0889eea9 100644
--- a/src/borg/helpers/parseformat.py
+++ b/src/borg/helpers/parseformat.py
@@ -117,6 +117,22 @@ def ChunkerParams(s):
     return int(chunk_min), int(chunk_max), int(chunk_mask), int(window_size)
 
 
+def FilesCacheMode(s):
+    ENTRIES_MAP = dict(ctime='c', mtime='m', size='s', inode='i', rechunk='r', disabled='d')
+    VALID_MODES = ('cis', 'ims', 'cs', 'ms', 'cr', 'mr', 'd')  # letters in alpha order
+    entries = set(s.strip().split(','))
+    if not entries <= set(ENTRIES_MAP):
+        raise ValueError('cache mode must be a comma-separated list of: %s' % ','.join(sorted(ENTRIES_MAP)))
+    short_entries = {ENTRIES_MAP[entry] for entry in entries}
+    mode = ''.join(sorted(short_entries))
+    if mode not in VALID_MODES:
+        raise ValueError('cache mode short must be one of: %s' % ','.join(VALID_MODES))
+    return mode
+
+
+assert FilesCacheMode(DEFAULT_FILES_CACHE_MODE_UI) == DEFAULT_FILES_CACHE_MODE  # keep these 2 values in sync!
+
+
 def partial_format(format, mapping):
     """
     Apply format.format_map(mapping) while preserving unknown keys

Files cache: use ctime instead of mtime
ctime can't be set via userspace, while mtime is easily manipulated.

For most files (e.g. not extracted from tarballs, or installed by a package manager) these two are the same, which limits the impact of the change on existing file caches.
- Works with sshfs? -> (YES) (see below)

@verygreen noted that in the Windows world ctime usually means "creation" not "change" time
- Works with windows+cygwin? -> YES
- Works with native windows? -> NO

diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py
index c9ba6605..11579377 100644
--- a/src/borg/testsuite/archiver.py
+++ b/src/borg/testsuite/archiver.py
@@ -318,8 +318,6 @@ def create_test_files(self):
         """Create a minimal test case including all supported file types
         """
         # File
-        self.create_regular_file('empty', size=0)
-        os.utime('input/empty', (MAX_S, MAX_S))
         self.create_regular_file('file1', size=1024 * 80)
         self.create_regular_file('flagfile', size=1024)
         # Directory
@@ -370,6 +368,8 @@ def create_test_files(self):
             if e.errno not in (errno.EINVAL, errno.ENOSYS):
                 raise
             have_root = False
+        time.sleep(1)  # "empty" must have newer timestamp than other files
+        self.create_regular_file('empty', size=0)
         return have_root
 
 
@@ -1591,9 +1591,8 @@ def test_file_status(self):
         """test that various file status show expected results
 
         clearly incomplete: only tests for the weird "unchanged" status for now"""
-        now = time.time()
         self.create_regular_file('file1', size=1024 * 80)
-        os.utime('input/file1', (now - 5, now - 5))  # 5 seconds ago
+        time.sleep(1)  # file2 must have newer timestamps than file1
         self.create_regular_file('file2', size=1024 * 80)
         self.cmd('init', '--encryption=repokey', self.repository_location)
         output = self.cmd('create', '--list', self.repository_location + '::test', 'input')
@@ -1606,12 +1605,51 @@ def test_file_status(self):
         # https://borgbackup.readthedocs.org/en/latest/faq.html#i-am-seeing-a-added-status-for-a-unchanged-file
         self.assert_in("A input/file2", output)
 
+    def test_file_status_cs_cache_mode(self):
+        """test that a changed file with faked "previous" mtime still gets backed up in ctime,size cache_mode"""
+        self.create_regular_file('file1', contents=b'123')
+        time.sleep(1)  # file2 must have newer timestamps than file1
+        self.create_regular_file('file2', size=10)
+        self.cmd('init', '--encryption=repokey', self.repository_location)
+        output = self.cmd('create', '--list', '--files-cache=ctime,size', self.repository_location + '::test1', 'input')
+        # modify file1, but cheat with the mtime (and atime) and also keep same size:
+        st = os.stat('input/file1')
+        self.create_regular_file('file1', contents=b'321')
+        os.utime('input/file1', ns=(st.st_atime_ns, st.st_mtime_ns))
+        # this mode uses ctime for change detection, so it should find file1 as modified
+        output = self.cmd('create', '--list', '--files-cache=ctime,size', self.repository_location + '::test2', 'input')
+        self.assert_in("A input/file1", output)
+
+    def test_file_status_ms_cache_mode(self):
+        """test that a chmod'ed file with no content changes does not get chunked again in mtime,size cache_mode"""
+        self.create_regular_file('file1', size=10)
+        time.sleep(1)  # file2 must have newer timestamps than file1
+        self.create_regular_file('file2', size=10)
+        self.cmd('init', '--encryption=repokey', self.repository_location)
+        output = self.cmd('create', '--list', '--files-cache=mtime,size', self.repository_location + '::test1', 'input')
+        # change mode of file1, no content change:
+        st = os.stat('input/file1')
+        os.chmod('input/file1', st.st_mode ^ stat.S_IRWXO)  # this triggers a ctime change, but mtime is unchanged
+        # this mode uses mtime for change detection, so it should find file1 as unmodified
+        output = self.cmd('create', '--list', '--files-cache=mtime,size', self.repository_location + '::test2', 'input')
+        self.assert_in("U input/file1", output)
+
+    def test_file_status_rc_cache_mode(self):
+        """test that files get rechunked unconditionally in rechunk,ctime cache mode"""
+        self.create_regular_file('file1', size=10)
+        time.sleep(1)  # file2 must have newer timestamps than file1
+        self.create_regular_file('file2', size=10)
+        self.cmd('init', '--encryption=repokey', self.repository_location)
+        output = self.cmd('create', '--list', '--files-cache=rechunk,ctime', self.repository_location + '::test1', 'input')
+        # no changes here, but this mode rechunks unconditionally
+        output = self.cmd('create', '--list', '--files-cache=rechunk,ctime', self.repository_location + '::test2', 'input')
+        self.assert_in("A input/file1", output)
+
     def test_file_status_excluded(self):
         """test that excluded paths are listed"""
 
-        now = time.time()
         self.create_regular_file('file1', size=1024 * 80)
-        os.utime('input/file1', (now - 5, now - 5))  # 5 seconds ago
+        time.sleep(1)  # file2 must have newer timestamps than file1
         self.create_regular_file('file2', size=1024 * 80)
         if has_lchflags:
             self.create_regular_file('file3', size=1024 * 80)
@@ -1647,9 +1685,8 @@ def test_create_json(self):
         assert 'stats' in archive
 
     def test_create_topical(self):
-        now = time.time()
         self.create_regular_file('file1', size=1024 * 80)
-        os.utime('input/file1', (now-5, now-5))
+        time.sleep(1)  # file2 must have newer timestamps than file1
         self.create_regular_file('file2', size=1024 * 80)
         self.cmd('init', '--encryption=repokey', self.repository_location)
         # no listing by default
@@ -2363,7 +2400,7 @@ def test_recreate_rechunkify(self):
             fd.write(b'b' * 280)
         self.cmd('init', '--encryption=repokey', self.repository_location)
         self.cmd('create', '--chunker-params', '7,9,8,128', self.repository_location + '::test1', 'input')
-        self.cmd('create', self.repository_location + '::test2', 'input', '--no-files-cache')
+        self.cmd('create', self.repository_location + '::test2', 'input', '--files-cache=disabled')
         list = self.cmd('list', self.repository_location + '::test1', 'input/large_file',
                         '--format', '{num_chunks} {unique_chunks}')
         num_chunks, unique_chunks = map(int, list.split(' '))
@@ -3513,7 +3550,6 @@ def define_common_options(add_common_option):
         add_common_option('-p', '--progress', dest='progress', action='store_true', help='foo')
         add_common_option('--lock-wait', dest='lock_wait', type=int, metavar='N', default=1,
                           help='(default: %(default)d).')
-        add_common_option('--no-files-cache', dest='no_files_cache', action='store_false', help='foo')
 
     @pytest.fixture
     def basic_parser(self):
@@ -3555,7 +3591,6 @@ def parse_vars_from_line(*line):
 
     def test_simple(self, parse_vars_from_line):
         assert parse_vars_from_line('--error') == {
-            'no_files_cache': True,
             'append': [],
             'lock_wait': 1,
             'log_level': 'error',
@@ -3563,7 +3598,6 @@ def test_simple(self, parse_vars_from_line):
         }
 
         assert parse_vars_from_line('--error', 'subcommand', '--critical') == {
-            'no_files_cache': True,
             'append': [],
             'lock_wait': 1,
             'log_level': 'critical',
@@ -3576,7 +3610,6 @@ def test_simple(self, parse_vars_from_line):
             parse_vars_from_line('--append-only', 'subcommand')
 
         assert parse_vars_from_line('--append=foo', '--append', 'bar', 'subcommand', '--append', 'baz') == {
-            'no_files_cache': True,
             'append': ['foo', 'bar', 'baz'],
             'lock_wait': 1,
             'log_level': 'warning',
@@ -3589,7 +3622,6 @@ def test_simple(self, parse_vars_from_line):
     @pytest.mark.parametrize('flag,args_key,args_value', (
         ('-p', 'progress', True),
         ('--lock-wait=3', 'lock_wait', 3),
-        ('--no-files-cache', 'no_files_cache', False),
     ))
     def test_flag_position_independence(self, parse_vars_from_line, position, flag, args_key, args_value):
         line = []
@@ -3600,7 +3632,6 @@ def test_flag_position_independence(self, parse_vars_from_line, position, flag,
             line.append(flag)
 
         result = {
-            'no_files_cache': True,
             'append': [],
             'lock_wait': 1,
             'log_level': 'warning',

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-xdist pytest-cov
apt-get update apt-get install -y gcc libacl1-dev libacl1 libssl-dev liblz4-dev libzstd-dev build-essential pkg-config python3-pkgconfig
3.9
requirements.d/development.txt requirements.d/docs.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.16
babel==2.17.0
-e git+https://github.com/borgbackup/borg.git@dcf5e77253d35ca625d53f356cd2b969f4f4c6fd#egg=borgbackup
cachetools==5.5.2
certifi==2025.1.31
chardet==5.2.0
charset-normalizer==3.4.1
colorama==0.4.6
coverage==7.8.0
Cython==3.0.12
distlib==0.3.9
docutils==0.21.2
exceptiongroup==1.2.2
execnet==2.1.1
filelock==3.18.0
guzzle_sphinx_theme==0.7.11
idna==3.10
imagesize==1.4.1
importlib_metadata==8.6.1
iniconfig==2.1.0
Jinja2==3.1.6
MarkupSafe==3.0.2
msgpack-python==0.5.6
packaging==24.2
platformdirs==4.3.7
pluggy==1.5.0
py-cpuinfo==9.0.0
Pygments==2.19.1
pyproject-api==1.9.0
pytest==8.3.5
pytest-benchmark==5.1.0
pytest-cov==6.0.0
pytest-xdist==3.6.1
pyzmq==26.3.0
requests==2.32.3
setuptools-scm==8.2.0
snowballstemmer==2.2.0
Sphinx==7.4.7
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
tomli==2.2.1
tox==4.25.0
typing_extensions==4.13.0
urllib3==2.3.0
virtualenv==20.29.3
zipp==3.21.0

name: borg
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.16
      - babel==2.17.0
      - cachetools==5.5.2
      - certifi==2025.1.31
      - chardet==5.2.0
      - charset-normalizer==3.4.1
      - colorama==0.4.6
      - coverage==7.8.0
      - cython==3.0.12
      - distlib==0.3.9
      - docutils==0.21.2
      - exceptiongroup==1.2.2
      - execnet==2.1.1
      - filelock==3.18.0
      - guzzle-sphinx-theme==0.7.11
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - jinja2==3.1.6
      - markupsafe==3.0.2
      - msgpack-python==0.5.6
      - packaging==24.2
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - py-cpuinfo==9.0.0
      - pygments==2.19.1
      - pyproject-api==1.9.0
      - pytest==8.3.5
      - pytest-benchmark==5.1.0
      - pytest-cov==6.0.0
      - pytest-xdist==3.6.1
      - pyzmq==26.3.0
      - requests==2.32.3
      - setuptools-scm==8.2.0
      - snowballstemmer==2.2.0
      - sphinx==7.4.7
      - sphinxcontrib-applehelp==2.0.0
      - sphinxcontrib-devhelp==2.0.0
      - sphinxcontrib-htmlhelp==2.1.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==2.0.0
      - sphinxcontrib-serializinghtml==2.0.0
      - tomli==2.2.1
      - tox==4.25.0
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - virtualenv==20.29.3
      - zipp==3.21.0
prefix: /opt/conda/envs/borg

src/borg/testsuite/archiver.py::ArchiverTestCase::test_file_status_cs_cache_mode src/borg/testsuite/archiver.py::ArchiverTestCase::test_file_status_ms_cache_mode src/borg/testsuite/archiver.py::ArchiverTestCase::test_file_status_rc_cache_mode src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_file_status_cs_cache_mode src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_file_status_ms_cache_mode src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_file_status_rc_cache_mode
src/borg/testsuite/archiver.py::test_return_codes[python] src/borg/testsuite/archiver.py::ArchiverTestCase::test_common_options src/borg/testsuite/archiver.py::ArchiverTestCase::test_compression_auto_compressible src/borg/testsuite/archiver.py::ArchiverTestCase::test_compression_auto_uncompressible src/borg/testsuite/archiver.py::ArchiverTestCase::test_compression_lz4_compressible src/borg/testsuite/archiver.py::ArchiverTestCase::test_compression_lzma_compressible src/borg/testsuite/archiver.py::ArchiverTestCase::test_compression_none_compressible src/borg/testsuite/archiver.py::ArchiverTestCase::test_compression_none_uncompressible src/borg/testsuite/archiver.py::ArchiverTestCase::test_compression_zlib_compressible src/borg/testsuite/archiver.py::ArchiverTestCase::test_corrupted_repository src/borg/testsuite/archiver.py::ArchiverTestCase::test_create_json src/borg/testsuite/archiver.py::ArchiverTestCase::test_create_no_cache_sync src/borg/testsuite/archiver.py::ArchiverTestCase::test_create_stdin src/borg/testsuite/archiver.py::ArchiverTestCase::test_debug_dump_archive src/borg/testsuite/archiver.py::ArchiverTestCase::test_debug_dump_manifest src/borg/testsuite/archiver.py::ArchiverTestCase::test_debug_info src/borg/testsuite/archiver.py::ArchiverTestCase::test_debug_refcount_obj src/borg/testsuite/archiver.py::ArchiverTestCase::test_delete_multiple src/borg/testsuite/archiver.py::ArchiverTestCase::test_detect_attic_repo src/borg/testsuite/archiver.py::ArchiverTestCase::test_info_json src/borg/testsuite/archiver.py::ArchiverTestCase::test_list_json src/borg/testsuite/archiver.py::ArchiverTestCase::test_list_repository_format src/borg/testsuite/archiver.py::ArchiverTestCase::test_list_size src/borg/testsuite/archiver.py::ArchiverTestCase::test_log_json src/borg/testsuite/archiver.py::ArchiverTestCase::test_recreate_rechunkify src/borg/testsuite/archiver.py::ArchiverTestCase::test_recreate_recompress src/borg/testsuite/archiver.py::ArchiverTestCase::test_recreate_target src/borg/testsuite/archiver.py::ArchiverCheckTestCase::test_check_usage src/borg/testsuite/archiver.py::ArchiverCheckTestCase::test_corrupted_manifest src/borg/testsuite/archiver.py::ArchiverCheckTestCase::test_manifest_rebuild_corrupted_chunk src/borg/testsuite/archiver.py::ArchiverCheckTestCase::test_missing_file_chunk src/borg/testsuite/archiver.py::ArchiverCheckTestCase::test_missing_manifest src/borg/testsuite/archiver.py::ManifestAuthenticationTest::test_disable src/borg/testsuite/archiver.py::ManifestAuthenticationTest::test_disable2 src/borg/testsuite/archiver.py::ManifestAuthenticationTest::test_not_required src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_common_options src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_compression_auto_compressible src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_compression_auto_uncompressible src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_compression_lz4_compressible src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_compression_lz4_uncompressible src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_compression_lzma_compressible src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_compression_lzma_uncompressible src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_compression_none_compressible src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_compression_none_uncompressible src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_compression_zlib_compressible src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_compression_zlib_uncompressible src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_create_json src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_create_no_cache_sync src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_create_stdin src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_debug_dump_archive src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_debug_dump_manifest src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_debug_info src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_debug_refcount_obj src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_delete_force src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_delete_multiple src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_detect_attic_repo src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_info_json src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_init_nested_repositories src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_list_json src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_list_repository_format src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_list_size src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_recreate_rechunkify src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_recreate_recompress src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_recreate_target src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_remote_repo_restrict_to_path src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_remote_repo_restrict_to_repository src/borg/testsuite/archiver.py::ArchiverCorruptionTestCase::test_cache_chunks src/borg/testsuite/archiver.py::ArchiverCorruptionTestCase::test_cache_files src/borg/testsuite/archiver.py::ArchiverCorruptionTestCase::test_chunks_archive src/borg/testsuite/archiver.py::ArchiverCorruptionTestCase::test_old_version_interfered src/borg/testsuite/archiver.py::DiffArchiverTestCase::test_sort_option
src/borg/testsuite/archiver.py::ArchiverTestCase::test_aes_counter_uniqueness_keyfile src/borg/testsuite/archiver.py::ArchiverTestCase::test_aes_counter_uniqueness_passphrase src/borg/testsuite/archiver.py::ArchiverTestCase::test_atime src/borg/testsuite/archiver.py::ArchiverTestCase::test_bad_filters src/borg/testsuite/archiver.py::ArchiverTestCase::test_basic_functionality src/borg/testsuite/archiver.py::ArchiverTestCase::test_benchmark_crud src/borg/testsuite/archiver.py::ArchiverTestCase::test_break_lock src/borg/testsuite/archiver.py::ArchiverTestCase::test_change_passphrase src/borg/testsuite/archiver.py::ArchiverTestCase::test_check_cache src/borg/testsuite/archiver.py::ArchiverTestCase::test_comment src/borg/testsuite/archiver.py::ArchiverTestCase::test_compression_lz4_uncompressible src/borg/testsuite/archiver.py::ArchiverTestCase::test_compression_lzma_uncompressible src/borg/testsuite/archiver.py::ArchiverTestCase::test_compression_zlib_uncompressible src/borg/testsuite/archiver.py::ArchiverTestCase::test_create_dry_run src/borg/testsuite/archiver.py::ArchiverTestCase::test_create_pattern src/borg/testsuite/archiver.py::ArchiverTestCase::test_create_pattern_exclude_folder_but_recurse src/borg/testsuite/archiver.py::ArchiverTestCase::test_create_pattern_exclude_folder_no_recurse src/borg/testsuite/archiver.py::ArchiverTestCase::test_create_pattern_file src/borg/testsuite/archiver.py::ArchiverTestCase::test_create_pattern_intermediate_folders_first src/borg/testsuite/archiver.py::ArchiverTestCase::test_create_pattern_root src/borg/testsuite/archiver.py::ArchiverTestCase::test_create_read_special_broken_symlink src/borg/testsuite/archiver.py::ArchiverTestCase::test_create_topical src/borg/testsuite/archiver.py::ArchiverTestCase::test_create_without_root src/borg/testsuite/archiver.py::ArchiverTestCase::test_debug_dump_archive_items src/borg/testsuite/archiver.py::ArchiverTestCase::test_debug_dump_repo_objs src/borg/testsuite/archiver.py::ArchiverTestCase::test_debug_profile src/borg/testsuite/archiver.py::ArchiverTestCase::test_debug_put_get_delete_obj src/borg/testsuite/archiver.py::ArchiverTestCase::test_delete src/borg/testsuite/archiver.py::ArchiverTestCase::test_delete_double_force src/borg/testsuite/archiver.py::ArchiverTestCase::test_delete_force src/borg/testsuite/archiver.py::ArchiverTestCase::test_delete_repo src/borg/testsuite/archiver.py::ArchiverTestCase::test_exclude_caches src/borg/testsuite/archiver.py::ArchiverTestCase::test_exclude_keep_tagged src/borg/testsuite/archiver.py::ArchiverTestCase::test_exclude_keep_tagged_deprecation src/borg/testsuite/archiver.py::ArchiverTestCase::test_exclude_normalization src/borg/testsuite/archiver.py::ArchiverTestCase::test_exclude_tagged src/borg/testsuite/archiver.py::ArchiverTestCase::test_export_tar src/borg/testsuite/archiver.py::ArchiverTestCase::test_export_tar_gz src/borg/testsuite/archiver.py::ArchiverTestCase::test_export_tar_strip_components src/borg/testsuite/archiver.py::ArchiverTestCase::test_export_tar_strip_components_links src/borg/testsuite/archiver.py::ArchiverTestCase::test_extract_hardlinks src/borg/testsuite/archiver.py::ArchiverTestCase::test_extract_include_exclude src/borg/testsuite/archiver.py::ArchiverTestCase::test_extract_include_exclude_regex src/borg/testsuite/archiver.py::ArchiverTestCase::test_extract_include_exclude_regex_from_file src/borg/testsuite/archiver.py::ArchiverTestCase::test_extract_list_output src/borg/testsuite/archiver.py::ArchiverTestCase::test_extract_pattern_opt src/borg/testsuite/archiver.py::ArchiverTestCase::test_extract_progress src/borg/testsuite/archiver.py::ArchiverTestCase::test_extract_with_pattern src/borg/testsuite/archiver.py::ArchiverTestCase::test_file_status src/borg/testsuite/archiver.py::ArchiverTestCase::test_file_status_excluded src/borg/testsuite/archiver.py::ArchiverTestCase::test_help src/borg/testsuite/archiver.py::ArchiverTestCase::test_info src/borg/testsuite/archiver.py::ArchiverTestCase::test_init_interrupt src/borg/testsuite/archiver.py::ArchiverTestCase::test_init_nested_repositories src/borg/testsuite/archiver.py::ArchiverTestCase::test_init_requires_encryption_option src/borg/testsuite/archiver.py::ArchiverTestCase::test_key_export_keyfile src/borg/testsuite/archiver.py::ArchiverTestCase::test_key_export_paperkey src/borg/testsuite/archiver.py::ArchiverTestCase::test_key_export_qr src/borg/testsuite/archiver.py::ArchiverTestCase::test_key_export_repokey src/borg/testsuite/archiver.py::ArchiverTestCase::test_key_import_errors src/borg/testsuite/archiver.py::ArchiverTestCase::test_key_import_paperkey src/borg/testsuite/archiver.py::ArchiverTestCase::test_list_chunk_counts src/borg/testsuite/archiver.py::ArchiverTestCase::test_list_format src/borg/testsuite/archiver.py::ArchiverTestCase::test_list_hash src/borg/testsuite/archiver.py::ArchiverTestCase::test_list_json_args src/borg/testsuite/archiver.py::ArchiverTestCase::test_list_prefix src/borg/testsuite/archiver.py::ArchiverTestCase::test_overwrite src/borg/testsuite/archiver.py::ArchiverTestCase::test_path_normalization src/borg/testsuite/archiver.py::ArchiverTestCase::test_progress_off src/borg/testsuite/archiver.py::ArchiverTestCase::test_progress_on src/borg/testsuite/archiver.py::ArchiverTestCase::test_prune_repository src/borg/testsuite/archiver.py::ArchiverTestCase::test_prune_repository_glob src/borg/testsuite/archiver.py::ArchiverTestCase::test_prune_repository_prefix src/borg/testsuite/archiver.py::ArchiverTestCase::test_prune_repository_save_space src/borg/testsuite/archiver.py::ArchiverTestCase::test_recreate_basic src/borg/testsuite/archiver.py::ArchiverTestCase::test_recreate_dry_run src/borg/testsuite/archiver.py::ArchiverTestCase::test_recreate_exclude_caches src/borg/testsuite/archiver.py::ArchiverTestCase::test_recreate_exclude_keep_tagged src/borg/testsuite/archiver.py::ArchiverTestCase::test_recreate_exclude_tagged src/borg/testsuite/archiver.py::ArchiverTestCase::test_recreate_list_output src/borg/testsuite/archiver.py::ArchiverTestCase::test_recreate_skips_nothing_to_do src/borg/testsuite/archiver.py::ArchiverTestCase::test_recreate_subtree_hardlinks src/borg/testsuite/archiver.py::ArchiverTestCase::test_recreate_target_rc src/borg/testsuite/archiver.py::ArchiverTestCase::test_rename src/borg/testsuite/archiver.py::ArchiverTestCase::test_repeated_files src/borg/testsuite/archiver.py::ArchiverTestCase::test_repository_move src/borg/testsuite/archiver.py::ArchiverTestCase::test_repository_swap_detection src/borg/testsuite/archiver.py::ArchiverTestCase::test_repository_swap_detection2 src/borg/testsuite/archiver.py::ArchiverTestCase::test_repository_swap_detection2_no_cache src/borg/testsuite/archiver.py::ArchiverTestCase::test_repository_swap_detection_no_cache src/borg/testsuite/archiver.py::ArchiverTestCase::test_repository_swap_detection_repokey_blank_passphrase src/borg/testsuite/archiver.py::ArchiverTestCase::test_security_dir_compat src/borg/testsuite/archiver.py::ArchiverTestCase::test_sparse_file src/borg/testsuite/archiver.py::ArchiverTestCase::test_strip_components src/borg/testsuite/archiver.py::ArchiverTestCase::test_strip_components_links src/borg/testsuite/archiver.py::ArchiverTestCase::test_symlink_extract src/borg/testsuite/archiver.py::ArchiverTestCase::test_umask src/borg/testsuite/archiver.py::ArchiverTestCase::test_unix_socket src/borg/testsuite/archiver.py::ArchiverTestCase::test_unknown_feature_on_cache_sync src/borg/testsuite/archiver.py::ArchiverTestCase::test_unknown_feature_on_change_passphrase src/borg/testsuite/archiver.py::ArchiverTestCase::test_unknown_feature_on_create src/borg/testsuite/archiver.py::ArchiverTestCase::test_unknown_feature_on_delete src/borg/testsuite/archiver.py::ArchiverTestCase::test_unknown_feature_on_read src/borg/testsuite/archiver.py::ArchiverTestCase::test_unknown_feature_on_rename src/borg/testsuite/archiver.py::ArchiverTestCase::test_unknown_mandatory_feature_in_cache src/borg/testsuite/archiver.py::ArchiverTestCase::test_unknown_unencrypted src/borg/testsuite/archiver.py::ArchiverTestCase::test_unusual_filenames src/borg/testsuite/archiver.py::ArchiverTestCase::test_usage src/borg/testsuite/archiver.py::ArchiverTestCase::test_with_lock src/borg/testsuite/archiver.py::ArchiverCheckTestCase::test_attic013_acl_bug src/borg/testsuite/archiver.py::ArchiverCheckTestCase::test_empty_repository src/borg/testsuite/archiver.py::ArchiverCheckTestCase::test_extra_chunks src/borg/testsuite/archiver.py::ArchiverCheckTestCase::test_manifest_rebuild_duplicate_archive src/borg/testsuite/archiver.py::ArchiverCheckTestCase::test_missing_archive_item_chunk src/borg/testsuite/archiver.py::ArchiverCheckTestCase::test_missing_archive_metadata src/borg/testsuite/archiver.py::ArchiverCheckTestCase::test_verify_data src/borg/testsuite/archiver.py::ArchiverCheckTestCase::test_verify_data_unencrypted src/borg/testsuite/archiver.py::ManifestAuthenticationTest::test_fresh_init_tam_required src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_aes_counter_uniqueness_keyfile src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_aes_counter_uniqueness_passphrase src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_atime src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_bad_filters src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_basic_functionality src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_benchmark_crud src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_break_lock src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_change_passphrase src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_check_cache src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_comment src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_corrupted_repository src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_create_dry_run src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_create_pattern src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_create_pattern_exclude_folder_but_recurse src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_create_pattern_exclude_folder_no_recurse src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_create_pattern_file src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_create_pattern_intermediate_folders_first src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_create_pattern_root src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_create_read_special_broken_symlink src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_create_topical src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_create_without_root src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_debug_dump_archive_items src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_debug_dump_repo_objs src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_debug_profile src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_delete src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_delete_double_force src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_delete_repo src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_exclude_caches src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_exclude_keep_tagged src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_exclude_keep_tagged_deprecation src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_exclude_normalization src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_exclude_tagged src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_export_tar src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_export_tar_gz src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_export_tar_strip_components src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_export_tar_strip_components_links src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_extract_hardlinks src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_extract_include_exclude src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_extract_include_exclude_regex src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_extract_include_exclude_regex_from_file src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_extract_list_output src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_extract_pattern_opt src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_extract_progress src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_extract_with_pattern src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_file_status src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_file_status_excluded src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_help src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_info src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_init_interrupt src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_init_requires_encryption_option src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_key_export_keyfile src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_key_export_paperkey src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_key_export_qr src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_key_export_repokey src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_key_import_errors src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_key_import_paperkey src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_list_chunk_counts src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_list_format src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_list_hash src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_list_json_args src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_list_prefix src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_log_json src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_overwrite src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_path_normalization src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_progress_off src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_progress_on src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_prune_repository src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_prune_repository_glob src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_prune_repository_prefix src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_prune_repository_save_space src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_recreate_basic src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_recreate_dry_run src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_recreate_exclude_caches src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_recreate_exclude_keep_tagged src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_recreate_exclude_tagged src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_recreate_list_output src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_recreate_skips_nothing_to_do src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_recreate_subtree_hardlinks src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_recreate_target_rc src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_rename src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_repeated_files src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_repository_move src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_repository_swap_detection src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_repository_swap_detection2 src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_repository_swap_detection2_no_cache src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_repository_swap_detection_no_cache src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_repository_swap_detection_repokey_blank_passphrase src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_security_dir_compat src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_sparse_file src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_strip_components src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_strip_components_doesnt_leak src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_strip_components_links src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_symlink_extract src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_umask src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_unix_socket src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_unknown_feature_on_cache_sync src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_unknown_feature_on_change_passphrase src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_unknown_feature_on_create src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_unknown_feature_on_delete src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_unknown_feature_on_read src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_unknown_feature_on_rename src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_unknown_mandatory_feature_in_cache src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_unknown_unencrypted src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_unusual_filenames src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_usage src/borg/testsuite/archiver.py::RemoteArchiverTestCase::test_with_lock src/borg/testsuite/archiver.py::DiffArchiverTestCase::test_basic_functionality src/borg/testsuite/archiver.py::test_get_args src/borg/testsuite/archiver.py::test_chunk_content_equal src/borg/testsuite/archiver.py::TestBuildFilter::test_basic src/borg/testsuite/archiver.py::TestBuildFilter::test_empty src/borg/testsuite/archiver.py::TestBuildFilter::test_strip_components src/borg/testsuite/archiver.py::TestCommonOptions::test_simple src/borg/testsuite/archiver.py::TestCommonOptions::test_flag_position_independence[-p-progress-True-before] src/borg/testsuite/archiver.py::TestCommonOptions::test_flag_position_independence[-p-progress-True-after] src/borg/testsuite/archiver.py::TestCommonOptions::test_flag_position_independence[-p-progress-True-both] src/borg/testsuite/archiver.py::TestCommonOptions::test_flag_position_independence[--lock-wait=3-lock_wait-3-before] src/borg/testsuite/archiver.py::TestCommonOptions::test_flag_position_independence[--lock-wait=3-lock_wait-3-after] src/borg/testsuite/archiver.py::TestCommonOptions::test_flag_position_independence[--lock-wait=3-lock_wait-3-both] src/borg/testsuite/archiver.py::test_parse_storage_quota src/borg/testsuite/archiver.py::test_help_formatting[benchmark src/borg/testsuite/archiver.py::test_help_formatting[benchmark-parser1] src/borg/testsuite/archiver.py::test_help_formatting[break-lock-parser2] src/borg/testsuite/archiver.py::test_help_formatting[change-passphrase-parser3] src/borg/testsuite/archiver.py::test_help_formatting[check-parser4] src/borg/testsuite/archiver.py::test_help_formatting[create-parser5] src/borg/testsuite/archiver.py::test_help_formatting[debug src/borg/testsuite/archiver.py::test_help_formatting[debug-parser16] src/borg/testsuite/archiver.py::test_help_formatting[delete-parser17] src/borg/testsuite/archiver.py::test_help_formatting[diff-parser18] src/borg/testsuite/archiver.py::test_help_formatting[export-tar-parser19] src/borg/testsuite/archiver.py::test_help_formatting[extract-parser20] src/borg/testsuite/archiver.py::test_help_formatting[help-parser21] src/borg/testsuite/archiver.py::test_help_formatting[info-parser22] src/borg/testsuite/archiver.py::test_help_formatting[init-parser23] src/borg/testsuite/archiver.py::test_help_formatting[key src/borg/testsuite/archiver.py::test_help_formatting[key-parser28] src/borg/testsuite/archiver.py::test_help_formatting[list-parser29] src/borg/testsuite/archiver.py::test_help_formatting[mount-parser30] src/borg/testsuite/archiver.py::test_help_formatting[prune-parser31] src/borg/testsuite/archiver.py::test_help_formatting[recreate-parser32] src/borg/testsuite/archiver.py::test_help_formatting[rename-parser33] src/borg/testsuite/archiver.py::test_help_formatting[serve-parser34] src/borg/testsuite/archiver.py::test_help_formatting[umount-parser35] src/borg/testsuite/archiver.py::test_help_formatting[upgrade-parser36] src/borg/testsuite/archiver.py::test_help_formatting[with-lock-parser37] src/borg/testsuite/archiver.py::test_help_formatting_helptexts[patterns-\nFile src/borg/testsuite/archiver.py::test_help_formatting_helptexts[placeholders-\nRepository src/borg/testsuite/archiver.py::test_help_formatting_helptexts[compression-\nIt
src/borg/archive.py src/borg/helpers/parseformat.py src/borg/constants.py docs/internals/data-structures.rst src/borg/archiver.py src/borg/cache.py
src/borg/archive.py src/borg/helpers/parseformat.py src/borg/constants.py docs/internals/data-structures.rst src/borg/archiver.py src/borg/cache.py