`borg2 prune`: Make time spans calendar-aware
- Dominant language
- Python
- Stars
- 13.7k
- Forks
- 875
- Avg merge
- 12h 37m
- Merged PRs (30d)
- 199
Description
Thanks to @Goddesen's amazing work with #8775 (❤️), `borg2 prune` now supports interval-based retention policies like `--keep-daily 1m` to keep daily archives within a one-month time span.
As discussed earlier in https://github.com/borgbackup/borg/pull/8775#discussion_r3389964169, `borg2 prune` uses a rather unusual definition of what a month and a year is. A month is treated as a fixed 31-day interval regardless of the actual calendar month, and a year as a fixed 365-day interval regardless of leap years. This yields rather surprising results: Assume today is 2026-07-22. A timespan of `12m` results in a 372-day period (i.e. 2025-07-15), instead of the expected 2025-07-22. A timespan of `3y` results in a 3 × 365 = 1,095-day period, which introduces a one-day offset because 2024 is a leap year. So instead of the expected 2023-07-22, Borg assumes 2023-07-23.
We initially assumed that this `1m == 31d` definition wasn't new (https://github.com/borgbackup/borg/pull/8775#discussion_r3409404045), but after digging into Borg's code, it turns out that it actually is. Although [`helpers.parseformat.interval()`](https://github.com/borgbackup/borg/blob/2079b6786e2fe4e43639f68a629f463382c74705/src/borg/helpers/parseformat.py#L130-L169) already existed before, it appears that it was dead code and wasn't used anywhere before. Other Borg code that utilises relative time spans, such as `borg2 repo-list --oldest/--newest/--older/--newer`, doesn't use `helpers.parseformat.interval()`, but [`helpers.time.calculate_relative_offset()`](https://github.com/borgbackup/borg/blob/2079b6786e2fe4e43639f68a629f463382c74705/src/borg/helpers/time.py#L125-L159) instead (via [`manifest.filter_archives_by_date()`](https://github.com/borgbackup/borg/blob/2079b6786e2fe4e43639f68a629f463382c74705/src/borg/manifest.py#L51-L77), initial implementation in #7272), which interprets months and years as calendar-aware units. Since we have added a note regarding this `1m == 31d` semantic to the [docs](https://github.com/borgbackup/borg/blob/2079b6786e2fe4e43639f68a629f463382c74705/docs/usage/general/date-time.rst.inc), it now matches `borg2 prune`, but is now incorrect for the rest of Borg using relative time spans.
I'd like to suggest using `helpers.time.calculate_relative_offset()` for `borg2 prune` as well. For `borg2 prune`, `from_ts` would naturally be the value of `--from`. For the `argparse` validator one might use [`helpers.parseformat.relative_time_marker_validator()`](https://github.com/borgbackup/borg/blob/2079b6786e2fe4e43639f68a629f463382c74705/src/borg/helpers/parseformat.py#L810-L816). Might be worth removing the `helpers.parseformat.interval()` function altogether then.
Since changing this later would be a breaking change, it should ideally be addressed during the Borg 2 beta phase.
Contributor guide
Research direction
Start with helpers.parseformat.interval(), helpers.time.calculate_relative_offset(), and manifest.filter_archives_by_date(), then trace how borg2 prune parses --keep-* values and --from. Compare the existing interval behavior with the calendar-aware path, including the relative_time_marker_validator() entry point. Done means prune applies calendar-aware month and year spans consistently and obsolete interval handling is addressed without leaving conflicting documentation.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- cli
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 58/100