Skip to main content

grit_lib/git_date/
mod.rs

1//! Git-compatible date parsing and display (ported from Git `date.c`).
2
3pub mod approx;
4pub(crate) mod compat;
5pub mod parse;
6pub mod show;
7pub mod tm;
8
9use show::{parse_date_format, DateMode, DateModeType};
10use std::mem::size_of;
11use tm::{atoi_bytes, parse_timestamp_prefix, Timestamp};
12
13/// Result of `test-tool date` — either lines for stdout or a process exit code (no output).
14pub enum TestToolDateResult {
15    Output(Vec<String>),
16    Exit(i32),
17}
18
19/// Run `test-tool date` (see `git/t/helper/test-date.c`).
20pub fn test_tool_date(args: &[String]) -> Result<TestToolDateResult, String> {
21    // Match Git's `test-lib.sh` (`TZ=UTC`) when harness sets `GIT_TEST_DATE_NOW` but leaves `TZ`
22    // unset (direct `sh t0006-date.sh` runs). Do not override an explicit `TZ` (e.g. `EST5`).
23    if std::env::var_os("GIT_TEST_DATE_NOW").is_some() && std::env::var_os("TZ").is_none() {
24        std::env::set_var("TZ", "UTC");
25        // POSIX: refresh libc timezone cache after changing TZ (not in all `libc` bindings).
26        unsafe extern "C" {
27            fn tzset();
28        }
29        unsafe {
30            tzset();
31        }
32    }
33    if args.is_empty() {
34        return Err("test-tool date: missing subcommand".to_string());
35    }
36    let sub = args[0].as_str();
37    let rest = &args[1..];
38
39    match sub {
40        "is64bit" => {
41            // Match Git's `test-tool date is64bit` (`test-date.c`): `sizeof(timestamp_t) == 8`.
42            let code = if size_of::<Timestamp>() == 8 { 0 } else { 1 };
43            Ok(TestToolDateResult::Exit(code))
44        }
45        "time_t-is64bit" => {
46            let code = if size_of::<compat::time_t>() == 8 {
47                0
48            } else {
49                1
50            };
51            Ok(TestToolDateResult::Exit(code))
52        }
53        "relative" => {
54            let mut lines = Vec::new();
55            for a in rest {
56                let t: u64 = a
57                    .parse()
58                    .map_err(|_| format!("test-tool date relative: bad integer {a}"))?;
59                let s = show::show_date_relative(t, tm::get_time_sec());
60                lines.push(format!("{a} -> {s}"));
61            }
62            Ok(TestToolDateResult::Output(lines))
63        }
64        "human" => {
65            let mut lines = Vec::new();
66            for a in rest {
67                let t: u64 = a
68                    .parse()
69                    .map_err(|_| format!("test-tool date human: bad integer {a}"))?;
70                let mut mode = DateMode::from_type(DateModeType::Human);
71                let s = show::show_date(t, 0, &mut mode);
72                show::date_mode_release(&mut mode);
73                lines.push(format!("{a} -> {s}"));
74            }
75            Ok(TestToolDateResult::Output(lines))
76        }
77        "parse" => {
78            let mut lines = Vec::new();
79            for a in rest {
80                match parse::parse_date(a) {
81                    Ok(ds) => {
82                        let parts: Vec<&str> = ds.split_whitespace().collect();
83                        if parts.len() >= 2 {
84                            let t: u64 = parts[0].parse().map_err(|_| "bad parse output")?;
85                            let tz = atoi_bytes(parts[1].as_bytes());
86                            let mut mode = DateMode::from_type(DateModeType::Iso8601);
87                            let out = show::show_date(t, tz, &mut mode);
88                            show::date_mode_release(&mut mode);
89                            lines.push(format!("{a} -> {out}"));
90                        } else {
91                            lines.push(format!("{a} -> bad"));
92                        }
93                    }
94                    Err(()) => lines.push(format!("{a} -> bad")),
95                }
96            }
97            Ok(TestToolDateResult::Output(lines))
98        }
99        "approxidate" => {
100            let mut lines = Vec::new();
101            for a in rest {
102                let mut err = 0;
103                let t = approx::approxidate_careful(a, Some(&mut err));
104                let mut mode = DateMode::from_type(DateModeType::Iso8601);
105                let out = show::show_date(t, 0, &mut mode);
106                show::date_mode_release(&mut mode);
107                lines.push(format!("{a} -> {out}"));
108            }
109            Ok(TestToolDateResult::Output(lines))
110        }
111        "timestamp" => {
112            let mut lines = Vec::new();
113            for a in rest {
114                let mut err = 0;
115                let t = approx::approxidate_careful(a, Some(&mut err));
116                lines.push(if err == 0 {
117                    format!("{a} -> {t}")
118                } else {
119                    format!("{a} -> bad")
120                });
121            }
122            Ok(TestToolDateResult::Output(lines))
123        }
124        s if s.starts_with("show:") => {
125            let format = s.strip_prefix("show:").unwrap_or("");
126            let mut mode = parse_date_format(format).map_err(|e| e.to_string())?;
127            let mut lines = Vec::new();
128            for a in rest {
129                let b = a.as_bytes();
130                let (t, n) = parse_timestamp_prefix(b);
131                let mut rest_b = &b[n..];
132                while rest_b.first() == Some(&b' ') {
133                    rest_b = &rest_b[1..];
134                }
135                let tz = atoi_bytes(rest_b);
136                let s = show::show_date(t, tz, &mut mode);
137                lines.push(format!("{a} -> {s}"));
138            }
139            show::date_mode_release(&mut mode);
140            Ok(TestToolDateResult::Output(lines))
141        }
142        _ => Err(format!("test-tool date: unknown subcommand '{sub}'")),
143    }
144}