Skip to content

fix: relative dates roll over month ends like Git, instead of clamping - #2910

Merged
Sebastian Thiel (Byron) merged 4 commits into
GitoxideLabs:mainfrom
codeAnqiang-ma:fix/relative-date-month-rollover
Aug 13, 2026
Merged

Sebastian Thiel (Byron) merged 4 commits into
GitoxideLabs:mainfrom
codeAnqiang-ma:fix/relative-date-month-rollover

Conversation

@codeAnqiang-ma

Copy link
Copy Markdown
Contributor

Fixes #2909.

gix_date::parse() used to merge all relative count-unit pairs into a single jiff::Span and subtract it with jiff's calendar arithmetic, which clamps to the end of the target month. Git's approxidate() instead steps down tm_mon/tm_year-style calendar fields while leaving the day-of-month alone, and lets mktime(3) roll a day beyond the end of the month over into the following month. Around month ends the two disagree by 1–3 days: one month before 2026-03-31 is 2026-03-03 to Git, not 2026-02-28.

What changed

  • parse_ago() now returns the recognized <count> <unit> pairs in input order instead of one merged Span.
  • subtract_pairs() applies them the way Git does: second through week subtract a fixed number of seconds from the timestamp, while month and year only step down the year/month fields of a struct tm-like Fields value whose day may temporarily point beyond the end of the month, and which normalizes like mktime(3) — rolling any excess days over into the following month. As in Git, each month/year pair first normalizes whatever the previous pairs left behind, pairs apply in input order, and repeated units accumulate.
  • Everything stays in UTC, as before; the (separate, already-known) local-time-zone question is untouched, and named dates (now, today, yesterday) are unaffected.
  • The various() test now uses a fixed now (the baseline's GIT_TEST_DATE_NOW=1000000000): its expected values are computed with jiff's clamping subtraction and would disagree with Git — and with this fix — when the test happens to run near the end of a longer month.
  • Error behaviour is preserved: too-large counts still fail with Couldn't parse span from '<unit> <count>', and the historical negative-span failure case is kept.

Why not extend the baseline instead?

The baseline fixture pins GIT_TEST_DATE_NOW=1000000000 (2001-09-09) for all relative dates, which can never produce a month-end rollover. The new months_and_years_roll_over_month_ends_like_git test therefore hard-codes values obtained from Git itself; each one reproduces with TZ=UTC GIT_TEST_DATE_NOW=<now> git rev-parse --since='<input>' (git 2.48.1), as noted in the test. I'm happy to extend the baseline format to support per-entry now values instead, if that's preferred.

Test evidence

The new test fails before the fix and passes after it:

$ git stash push -- gix-date/src/parse/relative.rs   # old implementation, new test
$ cargo test -p gix-date months_and_years
<1772280000 (clamped) vs >1772539200 (Git)
test result: FAILED. 0 passed; 1 failed
$ git stash pop

Full crate test suite:

$ cargo test -p gix-date
test result: ok. 55 passed; 0 failed; 0 ignored (unit + baseline)
test result: ok. 1 passed (doc-tests)

cargo clippy -p gix-date --all-targets is clean. Cross-checked all twelve table rows from #2909 (plus order/accumulation cases) against git rev-parse --since with pinned GIT_TEST_DATE_NOW: gix-date now matches Git exactly on every one.


Disclosure: this PR was prepared with AI assistance (also marked with an Assisted-by: commit trailer); I reviewed the change and verified every reproduction locally against git 2.48.1.

…g. (GitoxideLabs#2909)

`N months/years ago` now steps down the calendar fields while keeping the
day-of-month, so a day beyond the end of the target month rolls over into
the following month, the way `mktime(3)` normalizes it for Git: one month
before 2026-03-31 now is 2026-03-03, no longer 2026-02-28.

Count-unit pairs also apply in input order and accumulate when repeated,
both matching Git as well.

Assisted-by: Claude
Co-authored-by: Cursor <[email protected]>
@Byron
Sebastian Thiel (Byron) force-pushed the fix/relative-date-month-rollover branch from 630e40e to 79da29c Compare August 13, 2026 06:02
<!-- agent -->
- [P2] Guard month-total subtraction against overflow —
  gix-date/src/parse/relative.rs:170-170 When multiple pairs first move an
  ordinary current date into a BCE year and then supply a near-`i64::MAX` month
  count (for example, `2027 years 9223372036854775807 months ago` in 2026), this
  subtraction underflows and panics in debug builds instead of returning a parse
  error. Use checked arithmetic for the total calculation.

Assisted-by: GPT 5.6
Co-authored-by: GPT 5.6 <[email protected]>
@Byron
Sebastian Thiel (Byron) force-pushed the fix/relative-date-month-rollover branch from 79da29c to 5488d5c Compare August 13, 2026 07:07
Comment thread gix-date/src/parse/relative.rs Dismissed
@Byron
Sebastian Thiel (Byron) force-pushed the fix/relative-date-month-rollover branch from 5488d5c to fdac92f Compare August 13, 2026 07:22
@Byron

Byron commented Aug 13, 2026

Copy link
Copy Markdown
Member

Thanks a lot!

Please beware that there is no need to create an issue first, as this project believes that PR = Issue + Code.

This allows time computation to be timezone aware, just like in Git.

Assisted-by: GPT 5.6
Co-authored-by: GPT 5.6 <[email protected]>
Assisted-by: GPT 5.6
Co-authored-by: GPT 5.6 <[email protected]>
@Byron
Sebastian Thiel (Byron) force-pushed the fix/relative-date-month-rollover branch from fdac92f to 613ff86 Compare August 13, 2026 07:36
@Byron
Sebastian Thiel (Byron) merged commit 566fea1 into GitoxideLabs:main Aug 13, 2026
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gix-date: relative dates clamp at month ends where Git rolls over (1 month ago on March 31st is off by 3 days)

3 participants