Skip to content

Commit 28ac657

Browse files
committed
gix-{archive,date}: switch from time to jiff
This swaps out `time` for `jiff`. It doesn't completely remove `time` from the dependency tree. The last remaining use of `time` is in `prodash`, outside of the gitoxide project.
1 parent 1b9c30d commit 28ac657

File tree

12 files changed

+144
-131
lines changed

12 files changed

+144
-131
lines changed

Cargo.lock

Lines changed: 29 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,14 +164,12 @@ http-client-reqwest = ["gix/blocking-http-transport-reqwest-rust-tls"]
164164
## Use async client networking.
165165
gitoxide-core-async-client = ["gitoxide-core/async-client", "futures-lite"]
166166

167-
168167
[dependencies]
169168
anyhow = "1.0.42"
170169

171170
gitoxide-core = { version = "^0.39.1", path = "gitoxide-core" }
172171
gix-features = { version = "^0.38.2", path = "gix-features" }
173172
gix = { version = "^0.64.0", path = "gix", default-features = false }
174-
time = "0.3.23"
175173

176174
clap = { version = "4.1.1", features = ["derive", "cargo"] }
177175
clap_complete = "4.4.3"

gix-archive/Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ tar = ["dep:tar", "dep:gix-path"]
2121
tar_gz = ["tar", "dep:flate2"]
2222

2323
## Enable the `zip` archive format.
24-
zip = ["dep:zip", "dep:time"]
24+
zip = ["dep:zip"]
2525

2626

2727
[dependencies]
@@ -31,8 +31,8 @@ gix-path = { version = "^0.10.9", path = "../gix-path", optional = true }
3131
gix-date = { version = "^0.8.7", path = "../gix-date" }
3232

3333
flate2 = { version = "1.0.26", optional = true }
34-
zip = { version = "2.1.0", optional = true, default-features = false, features = ["deflate", "time"] }
35-
time = { version = "0.3.23", optional = true, default-features = false, features = ["std"] }
34+
zip = { version = "2.1.0", optional = true, default-features = false, features = ["deflate"] }
35+
jiff = { version = "0.1.2", default-features = false, features = ["std"] }
3636

3737
thiserror = "1.0.26"
3838
bstr = { version = "1.5.0", default-features = false }

gix-archive/src/write.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,22 @@ where
134134
{
135135
let mut ar = zip::write::ZipWriter::new(out);
136136
let mut buf = Vec::new();
137-
let mtime = time::OffsetDateTime::from_unix_timestamp(opts.modification_time)
137+
let zdt = jiff::Timestamp::from_second(opts.modification_time)
138138
.map_err(|err| Error::InvalidModificationTime(Box::new(err)))?
139-
.try_into()
140-
.map_err(|err| Error::InvalidModificationTime(Box::new(err)))?;
139+
.to_zoned(jiff::tz::TimeZone::UTC);
140+
let mtime = zip::DateTime::from_date_and_time(
141+
zdt.year()
142+
.try_into()
143+
.map_err(|err| Error::InvalidModificationTime(Box::new(err)))?,
144+
// These are all OK because month, day, hour, minute and second
145+
// are always positive.
146+
zdt.month().try_into().expect("non-negative"),
147+
zdt.day().try_into().expect("non-negative"),
148+
zdt.hour().try_into().expect("non-negative"),
149+
zdt.minute().try_into().expect("non-negative"),
150+
zdt.second().try_into().expect("non-negative"),
151+
)
152+
.map_err(|err| Error::InvalidModificationTime(Box::new(err)))?;
141153
while let Some(entry) = next_entry(stream)? {
142154
append_zip_entry(
143155
&mut ar,

gix-date/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ serde= ["dep:serde", "bstr/serde"]
2020
bstr = { version = "1.3.0", default-features = false, features = ["std"]}
2121
serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]}
2222
itoa = "1.0.1"
23-
time = { version = "0.3.23", default-features = false, features = ["local-offset", "formatting", "macros", "parsing"] }
23+
jiff = "0.1.1"
2424
thiserror = "1.0.32"
2525

2626
document-features = { version = "0.2.0", optional = true }

gix-date/src/parse.rs

Lines changed: 65 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub enum Error {
1414
pub(crate) mod function {
1515
use std::{str::FromStr, time::SystemTime};
1616

17-
use time::{format_description::well_known, Date, OffsetDateTime};
17+
use jiff::{civil::Date, fmt::rfc2822, tz::TimeZone, Zoned};
1818

1919
use crate::{
2020
parse::{relative, Error},
@@ -32,27 +32,27 @@ pub(crate) mod function {
3232
return Ok(Time::new(42, 1800));
3333
}
3434

35-
Ok(if let Ok(val) = Date::parse(input, SHORT.0) {
36-
let val = val.with_hms(0, 0, 0).expect("date is in range").assume_utc();
37-
Time::new(val.unix_timestamp(), val.offset().whole_seconds())
38-
} else if let Ok(val) = OffsetDateTime::parse(input, &well_known::Rfc2822) {
39-
Time::new(val.unix_timestamp(), val.offset().whole_seconds())
40-
} else if let Ok(val) = OffsetDateTime::parse(input, ISO8601.0) {
41-
Time::new(val.unix_timestamp(), val.offset().whole_seconds())
42-
} else if let Ok(val) = OffsetDateTime::parse(input, ISO8601_STRICT.0) {
43-
Time::new(val.unix_timestamp(), val.offset().whole_seconds())
44-
} else if let Ok(val) = OffsetDateTime::parse(input, GITOXIDE.0) {
45-
Time::new(val.unix_timestamp(), val.offset().whole_seconds())
46-
} else if let Ok(val) = OffsetDateTime::parse(input, DEFAULT.0) {
47-
Time::new(val.unix_timestamp(), val.offset().whole_seconds())
35+
Ok(if let Ok(val) = Date::strptime(SHORT.0, input) {
36+
let val = val.to_zoned(TimeZone::UTC).expect("date is in range");
37+
Time::new(val.timestamp().as_second(), val.offset().seconds())
38+
} else if let Ok(val) = rfc2822_relaxed(input) {
39+
Time::new(val.timestamp().as_second(), val.offset().seconds())
40+
} else if let Ok(val) = strptime_relaxed(ISO8601.0, input) {
41+
Time::new(val.timestamp().as_second(), val.offset().seconds())
42+
} else if let Ok(val) = strptime_relaxed(ISO8601_STRICT.0, input) {
43+
Time::new(val.timestamp().as_second(), val.offset().seconds())
44+
} else if let Ok(val) = strptime_relaxed(GITOXIDE.0, input) {
45+
Time::new(val.timestamp().as_second(), val.offset().seconds())
46+
} else if let Ok(val) = strptime_relaxed(DEFAULT.0, input) {
47+
Time::new(val.timestamp().as_second(), val.offset().seconds())
4848
} else if let Ok(val) = SecondsSinceUnixEpoch::from_str(input) {
4949
// Format::Unix
5050
Time::new(val, 0)
5151
} else if let Some(val) = parse_raw(input) {
5252
// Format::Raw
5353
val
54-
} else if let Some(time) = relative::parse(input, now).transpose()? {
55-
Time::new(time.unix_timestamp(), time.offset().whole_seconds())
54+
} else if let Some(val) = relative::parse(input, now).transpose()? {
55+
Time::new(val.timestamp().as_second(), val.offset().seconds())
5656
} else {
5757
return Err(Error::InvalidDateString { input: input.into() });
5858
})
@@ -83,52 +83,79 @@ pub(crate) mod function {
8383
};
8484
Some(time)
8585
}
86+
87+
/// This is just like `Zoned::strptime`, but it allows parsing datetimes
88+
/// whose weekdays are inconsistent with the date. While the day-of-week
89+
/// still must be parsed, it is otherwise ignored. This seems to be
90+
/// consistent with how `git` behaves.
91+
fn strptime_relaxed(fmt: &str, input: &str) -> Result<Zoned, jiff::Error> {
92+
let mut tm = jiff::fmt::strtime::parse(fmt, input)?;
93+
tm.set_weekday(None);
94+
tm.to_zoned()
95+
}
96+
97+
/// This is just like strptime_relaxed, except for RFC 2822 parsing.
98+
/// Namely, it permits the weekday to be inconsistent with the date.
99+
fn rfc2822_relaxed(input: &str) -> Result<Zoned, jiff::Error> {
100+
static P: rfc2822::DateTimeParser = rfc2822::DateTimeParser::new().relaxed_weekday(true);
101+
P.parse_zoned(input)
102+
}
86103
}
87104

88105
mod relative {
89106
use std::{str::FromStr, time::SystemTime};
90107

91-
use time::{Duration, OffsetDateTime};
108+
use jiff::{tz::TimeZone, Span, Timestamp, Zoned};
92109

93110
use crate::parse::Error;
94111

95-
fn parse_inner(input: &str) -> Option<Duration> {
112+
fn parse_inner(input: &str) -> Option<Result<Span, Error>> {
96113
let mut split = input.split_whitespace();
97-
let multiplier = i64::from_str(split.next()?).ok()?;
114+
let units = i64::from_str(split.next()?).ok()?;
98115
let period = split.next()?;
99116
if split.next()? != "ago" {
100117
return None;
101118
}
102-
duration(period, multiplier)
119+
span(period, units)
103120
}
104121

105-
pub(crate) fn parse(input: &str, now: Option<SystemTime>) -> Option<Result<OffsetDateTime, Error>> {
106-
parse_inner(input).map(|offset| {
107-
let offset = std::time::Duration::from_secs(offset.whole_seconds().try_into()?);
122+
pub(crate) fn parse(input: &str, now: Option<SystemTime>) -> Option<Result<Zoned, Error>> {
123+
parse_inner(input).map(|result| {
124+
let span = result?;
125+
// This was an error case in a previous version of this code, where
126+
// it would fail when converting from a negative signed integer
127+
// to an unsigned integer. This preserves that failure case even
128+
// though the code below handles it okay.
129+
if span.is_negative() {
130+
return Err(Error::RelativeTimeConversion);
131+
}
108132
now.ok_or(Error::MissingCurrentTime).and_then(|now| {
109-
std::panic::catch_unwind(|| {
110-
now.checked_sub(offset)
111-
.expect("BUG: values can't be large enough to cause underflow")
112-
.into()
113-
})
114-
.map_err(|_| Error::RelativeTimeConversion)
133+
let ts = Timestamp::try_from(now).map_err(|_| Error::RelativeTimeConversion)?;
134+
// N.B. This matches the behavior of this code when it was
135+
// written with `time`, but we might consider using the system
136+
// time zone here. If we did, then it would implement "1 day
137+
// ago" correctly, even when it crosses DST transitions. Since
138+
// we're in the UTC time zone here, which has no DST, 1 day is
139+
// in practice always 24 hours. ---AG
140+
let zdt = ts.to_zoned(TimeZone::UTC);
141+
zdt.checked_sub(span).map_err(|_| Error::RelativeTimeConversion)
115142
})
116143
})
117144
}
118145

119-
fn duration(period: &str, multiplier: i64) -> Option<Duration> {
146+
fn span(period: &str, units: i64) -> Option<Result<Span, Error>> {
120147
let period = period.strip_suffix('s').unwrap_or(period);
121-
let seconds: i64 = match period {
122-
"second" => 1,
123-
"minute" => 60,
124-
"hour" => 60 * 60,
125-
"day" => 24 * 60 * 60,
126-
"week" => 7 * 24 * 60 * 60,
148+
let result = match period {
149+
"second" => Span::new().try_seconds(units),
150+
"minute" => Span::new().try_minutes(units),
151+
"hour" => Span::new().try_hours(units),
152+
"day" => Span::new().try_days(units),
153+
"week" => Span::new().try_weeks(units),
127154
// TODO months & years? YES
128155
// Ignore values you don't know, assume seconds then (so does git)
129156
_ => return None,
130157
};
131-
seconds.checked_mul(multiplier).map(Duration::seconds)
158+
Some(result.map_err(|_| Error::RelativeTimeConversion))
132159
}
133160

134161
#[cfg(test)]
@@ -137,7 +164,7 @@ mod relative {
137164

138165
#[test]
139166
fn two_weeks_ago() {
140-
assert_eq!(parse_inner("2 weeks ago"), Some(Duration::weeks(2)));
167+
assert_eq!(parse_inner("2 weeks ago").unwrap().unwrap(), Span::new().weeks(2));
141168
}
142169
}
143170
}

0 commit comments

Comments
 (0)