Renames --since to --from

This commit is contained in:
Hugo Wallenburg
2026-06-20 18:36:59 +02:00
parent fe0d2d117d
commit c0a69611a8
5 changed files with 46 additions and 46 deletions
+10 -10
View File
@@ -21,20 +21,20 @@ borg prune \
--keep-weekly 4 \
--keep-monthly 5m \
--keep-yearly 2 \
--since '2026-06-04 16:00'
--from '2026-06-04 16:00'
```
The `--keep-*` options reflect the intended retention policy exactly. Note the
different wording in the retention policy for weekly and yearly archives: They
aren't *interval*-based, but *count*-based.
Another important detail here is `--since`. Without it, intervals would be
Another important detail here is `--from`. Without it, intervals would be
calculated relative to the actual start time of `prune` - in this case 16:12.
Since your backups are always created at 16:00, this 12-minute shift would move
the cutoff point of intervals and could cause archives near the boundary to
unexpectedly fall outside the expected time window.
By specifying `--since '2026-06-04 16:00'`, all intervals are anchored to the
By specifying `--from '2026-06-04 16:00'`, all intervals are anchored to the
intended reference time (16:00), not the moment `prune` happens to run. This
ensures stable and predictable retention behavior, independent of when `prune`
actually runs.
@@ -48,7 +48,7 @@ Archives kept by the `--keep-daily` rule are marked by a "d" to the right,
archives kept by the `--keep-weekly` rule are marked by a "w" to the right,
archives kept by the `--keep-monthly` rule are marked by a "m" to the right,
archives kept by the `--keep-yearly` rule are marked by a "y" to the right, and
archives kept by the `--since` rule are marked by a "x" to the right.
archives kept by the `--from` rule are marked by a "x" to the right.
Calendar view
@@ -91,25 +91,25 @@ List view
5. 2026-05-29 5. 2025-12-31
6. 2026-05-28
2026-06-04 is additionally kept due to `--since`.
2026-06-04 is additionally kept due to `--from`.
Notes
-----
The current day's archive is always kept, because `create` ran after the date
given with `--since`. For `prune`, it's as if this archive doesn't exist (yet).
given with `--from`. For `prune`, it's as if this archive doesn't exist (yet).
2026-06-03 was skipped, so no archive can be kept with `--keep-daily` for that
day. Other than with a *count*-based policy, no compensation is made for an
*interval* like `--keep-daily 1w`, so the rule simply keeps one archive fewer.
2026-05-28 16:00 is exactly one week before `--since`. Since `create` always
2026-05-28 16:00 is exactly one week before `--from`. Since `create` always
runs after 16:00, the archive created on 2026-05-28 is kept, too. Without
`--since`, Borg would cut off at 2026-05-28 16:12 instead, which would likely
mean that the archive created on 2026-05-28 would be pruned. `--since` ensures
`--from`, Borg would cut off at 2026-05-28 16:12 instead, which would likely
mean that the archive created on 2026-05-28 would be pruned. `--from` ensures
that 2026-05-28 is consistently kept. If you want it consistently pruned, try a
later reference time, e.g. `--since '2026-06-04 23:59:59'`.
later reference time, e.g. `--from '2026-06-04 23:59:59'`.
2026-05-31 is considered not only by `--keep-daily`, but by `--keep-weekly`
and `--keep-monthly`, too. The archive is effectively kept by `--keep-daily`,
+1 -1
View File
@@ -20,7 +20,7 @@ days (e.g. ``7d``), hours (e.g. ``8H``), minutes (e.g. ``30M``), or seconds
The ``borg prune`` ``--keep-*`` retention options accept either a plain count
(e.g. ``--keep-daily 7``, keeping up to 7 daily archives) or a time interval
(e.g. ``--keep-daily 7d``, keeping one daily archive per day within a 7-day window).
When using interval-based retention, ``--since`` may be specified to set the
When using interval-based retention, ``--from`` may be specified to set the
reference timestamp for the interval (defaults to the current time).
Please note that Borg treats months (e.g. ``12m``) as fixed 31-day periods
+2 -2
View File
@@ -27,7 +27,7 @@ The ``--keep-*`` options accept either a **count** (e.g. ``--keep-daily 7``) or
a **time interval** (e.g. ``--keep-daily 7d``). A count keeps up to *N* archives
per period (e.g. the last 7 daily archives), while an interval keeps one
archive per period within that time span (e.g. one daily archive per day in the
last 7-day window). When using intervals, you may also specify ``--since`` to
last 7-day window). When using intervals, you may also specify ``--from`` to
set the reference timestamp for interval calculation.
::
@@ -55,7 +55,7 @@ set the reference timestamp for interval calculation.
$ borg prune -v --list --dry-run --keep-daily=7d
# Same as above, but with a fixed reference timestamp:
$ borg prune -v --list --dry-run --since 2025-12-01T00:00:00+02:00 --keep-daily=7d
$ borg prune -v --list --dry-run --from 2025-12-01T00:00:00+02:00 --keep-daily=7d
# Keep the last 14 archives using `--keep`:
$ borg prune -v --list --dry-run --keep 14
+16 -16
View File
@@ -107,8 +107,8 @@ PRUNE_QUARTERLY_13WEEKLY = PruningRule("quarterly_13weekly", quarterly_13weekly_
PRUNE_QUARTERLY_3MONTHLY = PruningRule("quarterly_3monthly", quarterly_3monthly_period_func)
PRUNE_YEARLY = PruningRule("yearly", pattern_period_func("%Y"))
# Fake rule used to indicate archives skipped by --since
PRUNE_SINCE = PruningRule("skip", unique_period_func())
# Fake rule used to indicate archives skipped by --from
PRUNE_FROM = PruningRule("skip", unique_period_func())
PRUNING_RULES = [
PRUNE_KEEP,
@@ -182,18 +182,18 @@ class PruneMixIn:
# Archives to keep along with the rule that ensured them being kept
keep = {}
since = getattr(args, PRUNE_SINCE.key)
from_timestamp = getattr(args, PRUNE_FROM.key)
candidate_archives = archives
if since is not None:
base_timestamp = since
if from_timestamp is not None:
base_timestamp = from_timestamp
# `--since` is a prefilter: Archives made at or after this time are kept by default. They are not considered
# `--from` is a prefilter: Archives made at or after this time are kept by default. They are not considered
# for pruning at all and thus won't falsely occupy an active retention period.
for archive in archives:
if archive.ts < since:
if archive.ts < from_timestamp:
break
keep[archive] = KeepResult(rule=PRUNE_SINCE, idx=len(keep))
keep[archive] = KeepResult(rule=PRUNE_FROM, idx=len(keep))
candidate_archives = archives[len(keep) :]
else:
base_timestamp = datetime.now().astimezone()
@@ -366,7 +366,7 @@ class PruneMixIn:
policy. It accepts a count or a time interval for retention (e.g.
``10`` or ``7d``, ``4w``). With a count it keeps at most that many
recent archives; with an interval it keeps all archives created within
that time window. When ``--since`` is given together with an interval
that time window. When ``--from`` is given together with an interval
retention, the interval is measured backwards from that timestamp
instead of from the current time. See ``Date and Time`` docs for exact
INTERVAL format.
@@ -380,14 +380,14 @@ class PruneMixIn:
period, e.g. one per day or one per month until the retention count is
met). With a retention interval, they keep one archive per period
within that time span (e.g. at most one per day in a span of seven
days, even if some days had none) -- measured from ``--since`` if given,
days, even if some days had none) -- measured from ``--from`` if given,
otherwise from the current time. Specifying a count of ``-1`` (or the
word ``all``) means no limit. A zero count or zero-length interval
keeps nothing.
The ``--since`` option restricts pruning to archives older than the given
The ``--from`` option restricts pruning to archives older than the given
TIMESTAMP. Archives made at or after this timestamp are kept unconditionally
as a pre-filter. When ``--since`` is used together with interval-based
as a pre-filter. When ``--from`` is used together with interval-based
``--keep-*`` options (e.g. ``--keep-daily 7d``), the interval is measured
backwards from the given timestamp rather than from the current time.
Count-based retention does not count the unconditionally kept archives.
@@ -417,10 +417,10 @@ class PruneMixIn:
older history gradually thins out with time. For example,
``--keep-daily 7d --keep-weekly 4w --keep-monthly 6`` keeps an
archive per day for the past week, per week for the past month, and
one per month for six months after that. Combine this with ``--since``
one per month for six months after that. Combine this with ``--from``
to align time windows to calendar boundaries rather than the exact
moment you run prune for more predictable behavior of coarser rules:
``--keep-daily 7d --keep-weekly 4w --since $(date +%F)``.
``--keep-daily 7d --keep-weekly 4w --from $(date +%F)``.
Count-based retention keeps archives less bound to time. For instance,
``--keep-yearly 3`` retains 3 yearly archives however far back they
@@ -476,9 +476,9 @@ class PruneMixIn:
"Some keys are always present. Note: JSON can only represent text.",
)
subparser.add_argument(
"--since",
"--from",
metavar="TIMESTAMP",
dest=PRUNE_SINCE.key,
dest=PRUNE_FROM.key,
type=timestamp,
action=Highlander,
help="only consider archives older than this for pruning",
+17 -17
View File
@@ -154,14 +154,14 @@ def test_prune_repository_example_interval(archivers, request, backup_files):
"prune",
"--list",
"--dry-run",
"--since=2026-06-04T16:00:00+00:00",
"--from=2026-06-04T16:00:00+00:00",
"--keep-daily=1w",
"--keep-weekly=4",
"--keep-monthly=5m",
"--keep-yearly=2",
)
# 2026-06-04 is kept unconditionally by the --since prefilter.
# 2026-06-04 is kept unconditionally by the --from prefilter.
assert re.search(r"Keeping archive \(rule: skip #1\):\s+backup_2026-06-04", output)
daily_kept = [
@@ -530,14 +530,14 @@ def test_prune_keep_int_or_interval(archivers, request, backup_files, keep_arg):
archiver = request.getfixturevalue(archivers)
cmd(archiver, "repo-create", RK_ENCRYPTION)
dt = datetime(2023, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
# Two archives at the --since boundary prove the inclusive timestamp check.
# Two archives at the --from since boundary prove the inclusive timestamp check.
_create_archive_dt(archiver, backup_files, "test-1", dt)
_create_archive_dt(
archiver, backup_files, "test-2", dt - timedelta(microseconds=999999)
) # Would be pruned if `secondly`-rule was active.
_create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(seconds=1))
_create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(seconds=1, microseconds=1))
output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg)
output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg)
assert re.search(r"Keeping archive \(rule: skip #1\):\s+test-1", output)
assert re.search(r"Keeping archive \(rule: keep #1\):\s+test-2", output)
assert re.search(r"Keeping archive \(rule: keep #2\):\s+test-3", output)
@@ -553,7 +553,7 @@ def test_prune_keep_secondly_int_or_interval(archivers, request, backup_files, k
_create_archive_dt(archiver, backup_files, "test-2", dt - timedelta(seconds=1, microseconds=999999))
_create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(seconds=2))
_create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(seconds=2, microseconds=1))
output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg)
output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg)
assert re.search(r"Keeping archive \(rule: secondly #1\):\s+test-1", output)
assert re.search(r"Keeping archive \(rule: secondly #2\):\s+test-2", output)
assert re.search(r"Would prune:\s+test-3", output)
@@ -570,7 +570,7 @@ def test_prune_keep_minutely_int_or_interval(archivers, request, backup_files, k
_create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(minutes=2))
_create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(minutes=3))
_create_archive_dt(archiver, backup_files, "test-5", dt - timedelta(minutes=3, microseconds=1))
output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg)
output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg)
assert re.search(r"Keeping archive \(rule: minutely #1\):\s+test-1", output)
assert re.search(r"Keeping archive \(rule: minutely #2\):\s+test-2", output)
assert re.search(r"Would prune:\s+test-3", output)
@@ -588,7 +588,7 @@ def test_prune_keep_hourly_int_or_interval(archivers, request, backup_files, kee
_create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(hours=2))
_create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(hours=3))
_create_archive_dt(archiver, backup_files, "test-5", dt - timedelta(hours=3, microseconds=1))
output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg)
output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg)
assert re.search(r"Keeping archive \(rule: hourly #1\):\s+test-1", output)
assert re.search(r"Keeping archive \(rule: hourly #2\):\s+test-2", output)
assert re.search(r"Would prune:\s+test-3", output)
@@ -606,7 +606,7 @@ def test_prune_keep_daily_int_or_interval(archivers, request, backup_files, keep
_create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(days=2))
_create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(days=3))
_create_archive_dt(archiver, backup_files, "test-5", dt - timedelta(days=3, microseconds=1))
output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg)
output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg)
assert re.search(r"Keeping archive \(rule: daily #1\):\s+test-1", output)
assert re.search(r"Keeping archive \(rule: daily #2\):\s+test-2", output)
assert re.search(r"Would prune:\s+test-3", output)
@@ -624,7 +624,7 @@ def test_prune_keep_weekly_int_or_interval(archivers, request, backup_files, kee
_create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(days=14))
_create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(days=21))
_create_archive_dt(archiver, backup_files, "test-5", dt - timedelta(days=21, microseconds=1))
output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg)
output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg)
assert re.search(r"Keeping archive \(rule: weekly #1\):\s+test-1", output)
assert re.search(r"Keeping archive \(rule: weekly #2\):\s+test-2", output)
assert re.search(r"Would prune:\s+test-3", output)
@@ -642,7 +642,7 @@ def test_prune_keep_monthly_int_or_interval(archivers, request, backup_files, ke
_create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(days=62))
_create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(days=93))
_create_archive_dt(archiver, backup_files, "test-5", dt - timedelta(days=93, microseconds=1))
output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg)
output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg)
assert re.search(r"Keeping archive \(rule: monthly #1\):\s+test-1", output)
assert re.search(r"Keeping archive \(rule: monthly #2\):\s+test-2", output)
assert re.search(r"Would prune:\s+test-3", output)
@@ -661,7 +661,7 @@ def test_prune_keep_13weekly_int_or_interval(archivers, request, backup_files, k
_create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(days=182))
_create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(days=273))
_create_archive_dt(archiver, backup_files, "test-5", dt - timedelta(days=273, microseconds=1))
output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg)
output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg)
assert re.search(r"Keeping archive \(rule: quarterly_13weekly #1\):\s+test-1", output)
assert re.search(r"Keeping archive \(rule: quarterly_13weekly #2\):\s+test-2", output)
assert re.search(r"Would prune:\s+test-3", output)
@@ -680,7 +680,7 @@ def test_prune_keep_3monthly_int_or_interval(archivers, request, backup_files, k
_create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(days=275))
_create_archive_dt(archiver, backup_files, "test-5", dt - timedelta(days=275, microseconds=1))
# 275d is the interval from dt to the oldest kept monthly archive
output = cmd(archiver, "prune", "--list", "--short", "--dry-run", "--since", dt.isoformat(), keep_arg)
output = cmd(archiver, "prune", "--list", "--short", "--dry-run", "--from", dt.isoformat(), keep_arg)
assert re.search(r"Keeping archive \(rule: quarterly_3monthly #1\):\s+test-1", output)
assert re.search(r"Keeping archive \(rule: quarterly_3monthly #2\):\s+test-2", output)
assert re.search(r"Would prune:\s+test-3", output)
@@ -698,7 +698,7 @@ def test_prune_keep_yearly_int_or_interval(archivers, request, backup_files, kee
_create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(days=730))
_create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(days=1095))
_create_archive_dt(archiver, backup_files, "test-5", dt - timedelta(days=1095, microseconds=1))
output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg)
output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg)
assert re.search(r"Keeping archive \(rule: yearly #1\):\s+test-1", output)
assert re.search(r"Keeping archive \(rule: yearly #2\):\s+test-2", output)
assert re.search(r"Would prune:\s+test-3", output)
@@ -714,7 +714,7 @@ def test_prune_keep_daily_all(archivers, request, backup_files, keep_arg):
_create_archive_dt(archiver, backup_files, "test-1", dt - timedelta(days=1))
_create_archive_dt(archiver, backup_files, "test-2", dt - timedelta(days=2))
_create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(days=3))
output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg)
output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg)
assert re.search(r"Keeping archive \(rule: daily #1\):\s+test-1", output)
assert re.search(r"Keeping archive \(rule: daily #2\):\s+test-2", output)
assert re.search(r"Keeping archive \(rule: daily #3\):\s+test-3", output)
@@ -730,7 +730,7 @@ def test_prune_keep_flat_all(archivers, request, backup_files, keep_arg):
_create_archive_dt(archiver, backup_files, "test-2", dt - timedelta(microseconds=2))
_create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(days=3))
_create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(days=33333))
output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg)
output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg)
assert re.search(r"Keeping archive \(rule: keep #1\):\s+test-1", output)
assert re.search(r"Keeping archive \(rule: keep #2\):\s+test-2", output)
assert re.search(r"Keeping archive \(rule: keep #3\):\s+test-3", output)
@@ -824,7 +824,7 @@ def test_prune_int_rolling_schedule_oldest_retention():
assert archives[-1].ts.strftime("%m-%d") == "01-31"
def test_prune_since_prefiltered_archives_ignored_in_pruning(archivers, request, backup_files):
def test_prune_from_prefiltered_archives_ignored_in_pruning(archivers, request, backup_files):
archiver = request.getfixturevalue(archivers)
cmd(archiver, "repo-create", RK_ENCRYPTION)
dt = datetime(2024, 6, 6, 12, 0, 0, tzinfo=timezone.utc)
@@ -833,7 +833,7 @@ def test_prune_since_prefiltered_archives_ignored_in_pruning(archivers, request,
_create_archive_dt(archiver, backup_files, "test-b", dt - timedelta(hours=1))
_create_archive_dt(archiver, backup_files, "test-c", dt - timedelta(days=1))
output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), "--keep-daily=1")
output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), "--keep-daily=1")
# 'test-b' is kept, meaning 'test-a' was entirely skipped for pruning consideration.
# They would otherwise have occupied the same period.