fix: accept the '@' prefix and the relative-date forms Git accepts - #2889
Merged
Sebastian Thiel (Byron) merged 3 commits intoAug 7, 2026
Merged
Conversation
`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.
Sebastian Thiel (Byron)
force-pushed
the
date-at-epoch
branch
3 times, most recently
from
August 7, 2026 09:08
72dd994 to
64181cf
Compare
Sebastian Thiel (Byron)
enabled auto-merge
August 7, 2026 09:08
Sebastian Thiel (Byron)
force-pushed
the
date-at-epoch
branch
from
August 7, 2026 09:18
64181cf to
a094c4e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
@on epoch dates, so@1234567890and@1660874655 +0800parse.approxidate()does: units without regard to case, more than one<count> <unit>pair, any byte as a separator, counts spelled out, and an optional trailingago.2 HOURS agoresolved to two seconds ago.Commits
'@'prefix on epoch dates, like Git does.Git baseline, the
@prefixparse()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:Two paths reach a timestamp, which is worth stating because they differ:
@<seconds> ±HHMMis 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 at100000000.gix-datealready parses both1660874655 +0800and1234567890; 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 withgit 2.52.0andGIT_TEST_DATE_NOW=1000000000, againstgix-datebefore and after:git2 HOURS ago999992800999999998— two seconds9999928002 days 3 hours ago9998164009998164001.hour.ago999996400999996400two days ago999827200999827200last week999395200999395200Five rules account for those:
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.2 days 3 hours agois 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 agois 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 how2008.12.24stays a date.onetoten, as they are innumber_name[].zerois not among them, since the lookup starts at one, andzero days agois not a date to Git either.lastis a count of one.agois not required. Even2 days henceresolves into the past, as Git has no notion of a date in the future.That last rule needed care.
agois what still permits an unrecognised unit to count as seconds — without that condition,1745582210 +0200would read as a count of1745582210in the unknown unit0200, and never reachparse_raw(), which owns it. The absolute formats were checked explicitly for this:123456789,@1234567890,20080214T203045-04:00and 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-daterather than an expectation, taking it from 82 to 93.is_relative_date()in the test is widened to match, since1.hour.agois a relative date that does not end in" ago".Reverting
relative.rsalone makesbaseline::parse_compare_formatfail on one of the new entries — which one varies, as the baseline is aHashMap. On2 Days agoit readsdisagrees 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 417cargo fmt --check -p gix-dateNot addressed here, and a question
noon,midnight,tea,AMandPMare in Git'sspecial[]table and are still rejected here. They are the one group that resolves against the local time zone rather than UTC —noonwith the same frozen clock is999950400underTZ=UTC,999930600underAsia/Kolkataand999964800underAmerica/New_York, while1 hour agois 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 agopins. 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.