Skip to content

Commit

Permalink
Merge branch 'git_date_parse'
Browse files Browse the repository at this point in the history
  • Loading branch information
Byron committed Sep 3, 2022
2 parents 5869e9f + 590fcc9 commit 75591fb
Show file tree
Hide file tree
Showing 11 changed files with 324 additions and 19 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion git-date/Cargo.toml
Expand Up @@ -18,12 +18,14 @@ serde1 = ["serde", "bstr/serde1"]
bstr = { version = "0.2.13", default-features = false, features = ["std"]}
serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]}
itoa = "1.0.1"
time = { version = "0.3.2", default-features = false, features = ["local-offset", "formatting", "macros"] }
time = { version = "0.3.2", default-features = false, features = ["local-offset", "formatting", "macros", "parsing"] }
thiserror = "1.0.32"

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

[dev-dependencies]
git-testtools = { path = "../tests/tools"}
once_cell = "1.12.0"

[package.metadata.docs.rs]
all-features = true
Expand Down
5 changes: 3 additions & 2 deletions git-date/src/lib.rs
Expand Up @@ -13,8 +13,9 @@
///
pub mod time;

mod parse;
pub use parse::parse;
///
pub mod parse;
pub use parse::function::parse;

/// A timestamp with timezone.
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
Expand Down
139 changes: 131 additions & 8 deletions git-date/src/parse.rs
@@ -1,11 +1,134 @@
use crate::Time;

#[derive(thiserror::Error, Debug)]
#[allow(missing_docs)]
pub fn parse(input: &str) -> Option<Time> {
// TODO: actual implementation, this is just to not constantly fail
if input == "1979-02-26 18:30:00" {
Some(Time::new(42, 1800))
} else {
None
pub enum Error {
#[error("Cannot represent times before UNIX epoch at timestamp {timestamp}")]
TooEarly { timestamp: i64 },
#[error("Date string can not be parsed")]
InvalidDateString,
#[error("Dates past 2038 can not be represented.")]
InvalidDate(#[from] std::num::TryFromIntError),
#[error("Current time is missing.")]
MissingCurrentTime,
}

pub(crate) mod function {
use crate::parse::{relative, Error};
use crate::time::format::{DEFAULT, ISO8601, ISO8601_STRICT, RFC2822, SHORT};
use crate::time::Sign;
use crate::Time;
use std::convert::TryInto;
use std::str::FromStr;
use std::time::SystemTime;
use time::{Date, OffsetDateTime};

#[allow(missing_docs)]
pub fn parse(input: &str, now: Option<SystemTime>) -> Result<Time, Error> {
// TODO: actual implementation, this is just to not constantly fail
if input == "1979-02-26 18:30:00" {
return Ok(Time::new(42, 1800));
}

Ok(if let Ok(val) = Date::parse(input, SHORT) {
let val = val.with_hms(0, 0, 0).expect("date is in range").assume_utc();
Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds())
} else if let Ok(val) = OffsetDateTime::parse(input, RFC2822) {
Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds())
} else if let Ok(val) = OffsetDateTime::parse(input, ISO8601) {
Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds())
} else if let Ok(val) = OffsetDateTime::parse(input, ISO8601_STRICT) {
Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds())
} else if let Ok(val) = OffsetDateTime::parse(input, DEFAULT) {
Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds())
} else if let Ok(val) = u32::from_str(input) {
// Format::Unix
Time::new(val, 0)
} else if let Some(val) = parse_raw(input) {
// Format::Raw
val
} else if let Some(time) = relative::parse(input, now).transpose()? {
Time::new(timestamp(time)?, time.offset().whole_seconds())
} else {
return Err(Error::InvalidDateString);
})
}

fn timestamp(date: OffsetDateTime) -> Result<u32, Error> {
let timestamp = date.unix_timestamp();
if timestamp < 0 {
Err(Error::TooEarly { timestamp })
} else {
Ok(timestamp.try_into()?)
}
}

fn parse_raw(input: &str) -> Option<Time> {
let mut split = input.split_whitespace();
let seconds_since_unix_epoch: u32 = split.next()?.parse().ok()?;
let offset = split.next()?;
if offset.len() != 5 {
return None;
}
let sign = if &offset[..1] == "-" { Sign::Plus } else { Sign::Minus };
let hours: i32 = offset[1..3].parse().ok()?;
let minutes: i32 = offset[3..5].parse().ok()?;
let offset_in_seconds = hours * 3600 + minutes * 60;
let time = Time {
seconds_since_unix_epoch,
offset_in_seconds,
sign,
};
Some(time)
}
}

mod relative {
use crate::parse::Error;
use std::convert::TryInto;
use std::str::FromStr;
use std::time::SystemTime;
use time::{Duration, OffsetDateTime};

fn parse_inner(input: &str) -> Option<Duration> {
let mut split = input.split_whitespace();
let multiplier = i64::from_str(split.next()?).ok()?;
let period = split.next()?;
if split.next()? != "ago" {
return None;
}
duration(period, multiplier)
}

pub(crate) fn parse(input: &str, now: Option<SystemTime>) -> Option<Result<OffsetDateTime, Error>> {
parse_inner(input).map(|offset| {
let offset = std::time::Duration::from_secs(offset.whole_seconds().try_into().expect("positive value"));
now.ok_or(Error::MissingCurrentTime).map(|now| {
now.checked_sub(offset)
.expect("BUG: values can't be large enough to cause underflow")
.into()
})
})
}

fn duration(period: &str, multiplier: i64) -> Option<Duration> {
let period = period.strip_suffix('s').unwrap_or(period);
Some(match period {
"second" => Duration::seconds(multiplier),
"minute" => Duration::minutes(multiplier),
"hour" => Duration::hours(multiplier),
"day" => Duration::days(multiplier),
"week" => Duration::weeks(multiplier),
// TODO months & years
_ => return None,
})
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn two_weeks_ago() {
assert_eq!(parse_inner("2 weeks ago"), Some(Duration::weeks(2)));
}
}
}
46 changes: 46 additions & 0 deletions git-date/tests/fixtures/generate_git_date_baseline.sh
@@ -0,0 +1,46 @@
#!/bin/bash
set -eu -o pipefail

git init;

function baseline() {
local test_date=$1 # first argument is the date to test

git -c section.key="$test_date" config --type=expiry-date section.key && status=0 || status=$?
{
echo "$test_date"
echo "$status"
if [ $status == 0 ]
then
git -c section.key="$test_date" config --type=expiry-date section.key
else
echo "-1"
fi
} >> baseline.git
}

# success

# date formats following to https://git-scm.com/docs/git-log#Documentation/git-log.txt---dateltformatgt

# short
# ODO
#baseline '2022-08-22'
# rfc2822
baseline 'Thu, 18 Aug 2022 12:45:06 +0800'
# iso8601
baseline '2022-08-17 22:04:58 +0200'
# iso8601_strict
baseline '2022-08-17T21:43:13+08:00'
# default
baseline 'Thu Sep 04 2022 10:45:06 -0400'
# unix
baseline '123456789'
# raw
baseline '1660874655 +0800'

# failing

# empty_input
baseline ""

Git LFS file not shown
118 changes: 117 additions & 1 deletion git-date/tests/time/parse.rs
@@ -1,14 +1,130 @@
use bstr::{BString, ByteSlice};
use git_date::time::Sign;
use git_date::Time;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::str::FromStr;
use std::time::SystemTime;

type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;

static BASELINE: Lazy<HashMap<BString, (usize, BString)>> = Lazy::new(|| {
let base = git_testtools::scripted_fixture_repo_read_only("generate_git_date_baseline.sh").unwrap();

(|| -> Result<_> {
let mut map = HashMap::new();
let baseline = std::fs::read(base.join("baseline.git"))?;
let mut lines = baseline.lines();
while let Some(date_str) = lines.next() {
let exit_code = lines.next().expect("three lines per baseline").to_str()?.parse()?;
let output = lines.next().expect("three lines per baseline").into();
map.insert(date_str.into(), (exit_code, output));
}
Ok(map)
})()
.unwrap()
});

#[test]
fn baseline() {
for (pattern, (exit_code, output)) in BASELINE.iter() {
let res = git_date::parse(pattern.to_str().expect("valid pattern"), Some(SystemTime::now()));
assert_eq!(
res.is_ok(),
*exit_code == 0,
"{pattern:?} disagrees with baseline: {res:?}"
);
if *exit_code == 0 {
let actual = res.unwrap().seconds_since_unix_epoch;
let expected = u32::from_str(output.to_str().expect("valid utf")).expect("valid epoch value");
assert_eq!(actual, expected, "{pattern:?} disagrees with baseline: {actual:?}")
}
}
}

#[test]
fn special_time_is_ok_for_now() {
assert_eq!(
git_date::parse("1979-02-26 18:30:00").unwrap(),
git_date::parse("1979-02-26 18:30:00", Some(SystemTime::now())).unwrap(),
Time {
seconds_since_unix_epoch: 42,
offset_in_seconds: 1800,
sign: Sign::Plus,
}
);
}

#[test]
fn short() {
assert_eq!(
git_date::parse("1979-02-26", Some(SystemTime::now())).expect("parsed date"),
Time {
seconds_since_unix_epoch: 288835200,
offset_in_seconds: 0,
sign: Sign::Plus,
},
"could not parse with SHORT format"
);
}

#[test]
fn rfc2822() {
assert_eq!(
git_date::parse("Thu, 18 Aug 2022 12:45:06 +0800", None).expect("parsed rfc2822 string"),
Time {
seconds_since_unix_epoch: 1660797906,
offset_in_seconds: 28800,
sign: Sign::Plus,
},
"could not parse with RFC2822 format"
);
}

#[test]
fn invalid_dates_can_be_produced_without_current_time() {
assert!(matches!(
git_date::parse("foobar", None).unwrap_err(),
git_date::parse::Error::InvalidDateString
));
}

mod relative {
use git_date::parse::Error;
use git_date::time::Sign;
use std::time::SystemTime;
use time::{Duration, OffsetDateTime};

#[test]
#[should_panic] // TODO: fix
fn large_offsets_can_panic() {
git_date::parse("999999999999999 weeks ago", Some(std::time::UNIX_EPOCH)).ok();
}

#[test]
#[should_panic] // TODO: fix
fn large_offsets_can_panic_elsewhere() {
git_date::parse("9999999999 weeks ago", Some(std::time::UNIX_EPOCH)).ok();
}

#[test]
fn offset_leading_to_before_unix_epoch_cannot_be_represented() {
let err = git_date::parse("1 second ago", Some(std::time::UNIX_EPOCH)).unwrap_err();
assert!(matches!(err, Error::TooEarly{timestamp} if timestamp == -1));
}

#[test]
fn various() {
let now = Some(SystemTime::now());
let two_weeks_ago = git_date::parse("2 weeks ago", now).expect("valid time");
assert_eq!(Sign::Plus, two_weeks_ago.sign);
assert_eq!(0, two_weeks_ago.offset_in_seconds);
let expected = OffsetDateTime::from(now.unwrap()).saturating_sub(Duration::weeks(2));
// account for the loss of precision when creating `Time` with seconds
let expected = expected.replace_nanosecond(0).unwrap();
assert_eq!(
OffsetDateTime::from_unix_timestamp(two_weeks_ago.seconds_since_unix_epoch as i64).expect("valid datetime"),
expected,
"relative times differ"
);
}
}
5 changes: 3 additions & 2 deletions git-repository/src/repository/identity.rs
@@ -1,4 +1,5 @@
use std::borrow::Cow;
use std::time::SystemTime;

use crate::{bstr::BString, permission};

Expand Down Expand Up @@ -125,13 +126,13 @@ impl Personas {
committer_email = committer_email.or_else(|| env_var("GIT_COMMITTER_EMAIL"));
committer_date = std::env::var("GIT_COMMITTER_DATE")
.ok()
.and_then(|date| git_date::parse(&date));
.and_then(|date| git_date::parse(&date, Some(SystemTime::now())).ok());

author_name = author_name.or_else(|| env_var("GIT_AUTHOR_NAME"));
author_email = author_email.or_else(|| env_var("GIT_AUTHOR_EMAIL"));
author_date = std::env::var("GIT_AUTHOR_DATE")
.ok()
.and_then(|date| git_date::parse(&date));
.and_then(|date| git_date::parse(&date, Some(SystemTime::now())).ok());

user_email = user_email.or_else(|| env_var("EMAIL")); // NOTE: we don't have permission for this specific one…
}
Expand Down

0 comments on commit 75591fb

Please sign in to comment.