Skip to main content

grit_lib/git_date/
mod.rs

1//! Git-compatible date parsing and display.
2
3pub mod approx;
4pub(crate) mod compat;
5pub mod parse;
6pub mod show;
7pub mod tm;
8
9use thiserror::Error;
10
11/// Failure to interpret a Git-style date cutoff string (for example `--since` / `--until`).
12#[derive(Clone, Debug, Eq, PartialEq, Error)]
13pub enum DateSpecError {
14    /// The specification could not be parsed as a date.
15    #[error("unparseable date spec: {spec:?}")]
16    Unparseable {
17        /// Original input string.
18        spec: String,
19    },
20}
21
22/// Parse a Git-style date cutoff using an injected reference time.
23///
24/// Relative forms such as `3.days.ago`, `yesterday`, and `noon` are resolved against
25/// `now_sec` in the **process timezone** (same as Git `approxidate`). Absolute forms
26/// include `@<unix-seconds>`, ISO 8601, and RFC 2822.
27///
28/// Unlike Git's silent fallback to "now" on some invalid input, this returns
29/// [`DateSpecError::Unparseable`] when the approxidate parser sets its error flag.
30///
31/// # Parameters
32///
33/// - `spec` — Git approxidate string (for example `2.weeks.ago`, `yesterday`, `@1700000000`).
34/// - `now_sec` — Unix seconds used as “now” when resolving relative fields.
35///
36/// # Returns
37///
38/// Unix timestamp in seconds for the cutoff instant (commits strictly after this time
39/// match a `--since` filter).
40///
41/// # Errors
42///
43/// Returns [`DateSpecError::Unparseable`] when the spec cannot be parsed or the
44/// resulting timestamp does not fit in [`i64`].
45///
46/// # Examples
47///
48/// ```
49/// use grit_lib::config::git_date::parse_date_cutoff;
50///
51/// let now = 1_700_100_000_i64;
52/// let cutoff = parse_date_cutoff("1.day.ago", now).expect("relative cutoff");
53/// assert_eq!(cutoff, now - 86_400);
54/// ```
55pub fn parse_date_cutoff(spec: &str, now_sec: i64) -> Result<i64, DateSpecError> {
56    let trimmed = spec.trim();
57    let mut err = 0;
58    let ts = approx::approxidate_careful_at(trimmed, now_sec, Some(&mut err));
59    if err != 0 {
60        return Err(DateSpecError::Unparseable {
61            spec: spec.to_string(),
62        });
63    }
64    i64::try_from(ts).map_err(|_| DateSpecError::Unparseable {
65        spec: spec.to_string(),
66    })
67}
68
69#[cfg(test)]
70mod tests {
71    use super::parse_date_cutoff;
72    use super::DateSpecError;
73
74    const NOW: i64 = 1_700_100_000;
75
76    #[test]
77    fn cutoff_rejects_garbage() {
78        assert_eq!(
79            parse_date_cutoff("", NOW),
80            Err(DateSpecError::Unparseable {
81                spec: String::new()
82            })
83        );
84        assert!(matches!(
85            parse_date_cutoff("!!!not-a-date!!!", NOW),
86            Err(DateSpecError::Unparseable { .. })
87        ));
88    }
89}