Skip to content

fix: accept the '@' prefix and the relative-date forms Git accepts - #2889

Merged
Sebastian Thiel (Byron) merged 3 commits into
GitoxideLabs:mainfrom
ameyypawar:date-at-epoch
Aug 7, 2026
Merged

fix: accept the '@' prefix and the relative-date forms Git accepts#2889
Sebastian Thiel (Byron) merged 3 commits into
GitoxideLabs:mainfrom
ameyypawar:date-at-epoch

Conversation

@ameyypawar

@ameyypawar Amey Pawar (ameyypawar) commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Created by Claude Code on behalf of Amey, who reviewed it before submitting. Everything below this line is the agent's writing, not his.


Summary

  • Accept a leading @ on epoch dates, so @1234567890 and @1660874655 +0800 parse.
  • Read relative dates the way approxidate() does: units without regard to case, more than one <count> <unit> pair, any byte as a separator, counts spelled out, and an optional trailing ago.
  • One of those was a wrong answer rather than a rejection: 2 HOURS ago resolved to two seconds ago.

Commits

  • fix: accept the '@' prefix on epoch dates, like Git does.
  • fix: accept the relative dates Git accepts, and read their units case-insensitively.

Git baseline, the @ prefix

parse() is documented as parsing "any time that Git can parse when inputting a date", and @ is the one prefix Git looks for before anything else:

if (*date == '@' &&
    !match_object_header_date(date + 1, timestamp, offset))
	return 0; /* success */

Two paths reach a timestamp, which is worth stating because they differ:

  • @<seconds> ±HHMM is handled by that fast path, for any value of <seconds>.
  • @<seconds> on its own is not — the @ is skipped as an unmatched byte in the loop below, and the digits are recognised only by the epoch heuristic, which starts at 100000000.

gix-date already parses both 1660874655 +0800 and 1234567890; only the prefix was missing, so the change strips it and reuses those parsers.

Git baseline, relative dates

approxidate_str() walks the input applying whatever it recognises, so several shapes reach the same answer. Measured with git 2.52.0 and GIT_TEST_DATE_NOW=1000000000, against gix-date before and after:

input git before after
2 HOURS ago 999992800 999999998 — two seconds 999992800
2 days 3 hours ago 999816400 rejected 999816400
1.hour.ago 999996400 rejected 999996400
two days ago 999827200 rejected 999827200
last week 999395200 rejected 999395200

Five rules account for those:

  • Unit names are compared with match_string(), which folds case. Uppercase units used to miss the match and fall through to a catch-all that counted them as seconds, which is the first row.
  • A unit is applied the moment it is seen, and reading continues, so 2 days 3 hours ago is both of them.
  • approxidate_alpha() ends a word at the first byte that is not a letter, so any other byte separates the parts. A leading - is one, which is why -1 days ago is one day rather than a rejection. This holds for the <count> <unit> shapes here; approxidate_digit() does look at the byte after a digit run, which is how 2008.12.24 stays a date.
  • Counts may be spelled out, from one to ten, as they are in number_name[]. zero is not among them, since the lookup starts at one, and zero days ago is not a date to Git either. last is a count of one.
  • The trailing ago is not required. Even 2 days hence resolves into the past, as Git has no notion of a date in the future.

That last rule needed care. ago is what still permits an unrecognised unit to count as seconds — without that condition, 1745582210 +0200 would read as a count of 1745582210 in the unknown unit 0200, and never reach parse_raw(), which owns it. The absolute formats were checked explicitly for this: 123456789, @1234567890, 20080214T203045-04:00 and RFC2822 all still parse as they did.

Validation

Eleven entries are added to the existing baseline, which records Git's own answer via git config --type=expiry-date rather than an expectation, taking it from 82 to 93. is_relative_date() in the test is widened to match, since 1.hour.ago is a relative date that does not end in " ago".

Reverting relative.rs alone makes baseline::parse_compare_format fail on one of the new entries — which one varies, as the baseline is a HashMap. On 2 Days ago it reads disagrees with baseline seconds since epoch: 999999998, the wrong answer in the assertion.

A differential sweep of 84 inputs against the same oracle goes from 29 agreeing to 70. The 14 that remain are the eleven special[] words below, and three where an unrecognised unit counts as seconds here and does something else entirely in Git.

  • cargo test — gix-date 57, gix-actor 19, gix-object 148, gix 417
  • cargo fmt --check -p gix-date

Not addressed here, and a question

noon, midnight, tea, AM and PM are in Git's special[] table and are still rejected here. They are the one group that resolves against the local time zone rather than UTC — noon with the same frozen clock is 999950400 under TZ=UTC, 999930600 under Asia/Kolkata and 999964800 under America/New_York, while 1 hour ago is the same everywhere.

subtract_span() deliberately works in UTC, with a note saying as much. Adding these means either implementing them in UTC, where they would still not agree with Git, or giving this parser a notion of local time. That seemed like your call rather than something to decide inside a bug fix, so they are left out — happy to add them either way if you have a preference.

One smaller thing, left alone: an unrecognised unit counts as seconds here, which 12345 florx ago pins. Git does not do that — it leaves the count pending, where it stands in for a field of the date itself. The comment claiming Git agreed has been corrected, but the behaviour is unchanged.

@ameyypawar Amey Pawar (ameyypawar) changed the title fix: accept the '@' prefix on epoch dates, like Git does fix: accept the '@' prefix and the relative-date forms Git accepts Aug 6, 2026
`parse()` is documented as parsing any time that Git can parse, and `@` is the
one prefix Git checks before anything else in `parse_date_basic()`. Two paths
reach a timestamp there: `@<seconds> ±HHMM` is taken by that fast path for any
value, while a bare `@<seconds>` is skipped as an unmatched byte and recognised
only by the epoch heuristic that starts at 100000000.

Both forms were rejected here even though the underlying `1660874655 +0800` and
`1234567890` formats already parse, so the prefix is stripped and those parsers
reused.

Also corrects the relative-date documentation, which advertised `1 hour from
now` although only `now`, `today`, `yesterday` and `<n> <unit> ago` are
understood.
…-insensitively.

Five shapes `approxidate()` in Git's `date.c` accepts were not accepted here.

Units are now matched without regard to case, as `match_string()` does. This one
produced a wrong answer rather than a rejection: `2 HOURS ago` fell through to
the catch-all and became two *seconds* ago, which looks like a date rather than
an error.

More than one `<count> <unit>` pair is read, so `2 days 3 hours ago` is both of
them. Git applies a unit the moment it sees one and carries on, and stopping
after the first pair would substitute a plausible-looking two days.

Any byte that is neither a digit nor a letter separates the parts, because
`approxidate_alpha()` ends a word at the first byte that is not a letter. So
`1.hour.ago` and `1-hour-ago` read as `1 hour ago` does, and a leading `-` is a
separator too, which is why `-1 days ago` is one day rather than a rejection.
Note this holds for the `<count> <unit>` shapes handled here; `approxidate_digit()`
does look at the byte after a digit run, which is how `2008.12.24` stays a date.

Counts may be spelled out, from `one` to `ten`, as they are in `number_name[]`.
`zero` is not among them, since Git's lookup starts at one. `last` is a count of
one, so `last week` resolves.

The trailing `ago` is not required, because Git applies a unit as soon as it sees
one; even `2 days hence` resolves into the past there. It is still what permits an
unknown unit to count as seconds, though, or `1745582210 +0200` would parse as a
count in the unknown unit `0200` and never reach `parse_raw()`.

Seven entries are added to the baseline, so these record Git's own answers. The
comment claiming Git reads an unknown unit as seconds is corrected: it does not,
and leaves the count pending, where it stands in for a field of the date itself.

Not addressed: `noon`, `midnight`, `tea`, `AM` and `PM` are in Git's `special[]`
table and still rejected here. They resolve against the local time zone, and this
parser deliberately works in UTC.
@Byron
Sebastian Thiel (Byron) force-pushed the date-at-epoch branch 3 times, most recently from 72dd994 to 64181cf Compare August 7, 2026 09:08
Assisted-by: GPT 5.6
Co-authored-by: GPT 5.6 <codex@openai.com>
@Byron
Sebastian Thiel (Byron) merged commit bde37c1 into GitoxideLabs:main Aug 7, 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.

2 participants