Summary
BackupOptions::excludes.globs is named like an exclude list but uses whitelist semantics inherited from ignore::overrides::OverrideBuilder. A bare pattern means "include only files matching this; exclude everything else", not "exclude files matching this".
This is a footgun because it fails silently — the backup completes without error, the snapshot exists, but it contains zero files.
Minimal reproducer
let mut opts = BackupOptions::default();
opts.excludes.globs = vec!["**/*.tmp".into()];
// open repo, indexed_ids, backup ...
let snap = repository.backup(&opts, &pathlist, snapshot_opts)?;
// snap.summary.total_files_processed == 0
// despite the source containing file1.txt, file2.txt, etc.
The user expected **/*.tmp to exclude .tmp files. Instead, all non-.tmp files were silently dropped.
Working form (current API)
To actually exclude .tmp files, prefix the pattern with !:
opts.excludes.globs = vec!["!**/*.tmp".into()];
Why it's harmful
- Field name implies the wrong meaning.
Excludes::globs reads as "patterns to exclude", not "patterns to include exclusively".
- Silent failure. No error is raised; the snapshot is created but empty. Detection only happens via
rustic ls <snapshot> or restore.
- Mismatch with restic conventions. restic uses
--exclude with the inverse semantics (bare = exclude, ! = re-include). Users coming from restic fall straight into this.
- Underdocumented. The doc comment on
Excludes::globs is /// Glob pattern to exclude/include without flagging that the default is include-only.
Suggestions (least to most invasive)
- Doc: explicitly state the whitelist semantics in the doc comment, with an example showing the
! prefix to actually exclude.
- Add a parallel field, e.g.
Excludes::globs_blacklist: Vec<String>, where entries are auto-prefixed with ! in as_override().
- Invert the semantics to match the field name: bare = exclude,
!pattern = include. This is a breaking change but matches user mental model and restic CLI.
Workaround
Downstream consumers can prefix ! to every user-provided pattern before assigning to excludes.globs. That's what we ended up doing in our project.
Summary
BackupOptions::excludes.globsis named like an exclude list but uses whitelist semantics inherited fromignore::overrides::OverrideBuilder. A bare pattern means "include only files matching this; exclude everything else", not "exclude files matching this".This is a footgun because it fails silently — the backup completes without error, the snapshot exists, but it contains zero files.
Minimal reproducer
The user expected
**/*.tmpto exclude.tmpfiles. Instead, all non-.tmpfiles were silently dropped.Working form (current API)
To actually exclude
.tmpfiles, prefix the pattern with!:Why it's harmful
Excludes::globsreads as "patterns to exclude", not "patterns to include exclusively".rustic ls <snapshot>or restore.--excludewith the inverse semantics (bare = exclude,!= re-include). Users coming from restic fall straight into this.Excludes::globsis/// Glob pattern to exclude/includewithout flagging that the default is include-only.Suggestions (least to most invasive)
!prefix to actually exclude.Excludes::globs_blacklist: Vec<String>, where entries are auto-prefixed with!inas_override().!pattern= include. This is a breaking change but matches user mental model and restic CLI.Workaround
Downstream consumers can prefix
!to every user-provided pattern before assigning toexcludes.globs. That's what we ended up doing in our project.