Skip to content

Make author and committer date roundtrip #1935

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -152,6 +152,7 @@ gitoxide-core-async-client = ["gitoxide-core/async-client", "futures-lite"]
anyhow = "1.0.42"

gitoxide-core = { version = "^0.46.0", path = "gitoxide-core" }
gix-date= { version = "^0.9.4", path = "gix-date" }
gix-features = { version = "^0.42.0", path = "gix-features" }
gix = { version = "^0.71.0", path = "gix", default-features = false }

2 changes: 1 addition & 1 deletion examples/log.rs
Original file line number Diff line number Diff line change
@@ -150,7 +150,7 @@ fn run(args: Args) -> anyhow::Result<()> {
commit_ref.author.actor().write_to(&mut buf)?;
buf.into()
},
time: commit_ref.author.time.format(format::DEFAULT),
time: gix_date::Time::from_bytes(commit_ref.author.time)?.format(format::DEFAULT),
message: commit_ref.message.to_owned(),
})
}),
1 change: 1 addition & 0 deletions gitoxide-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -50,6 +50,7 @@ serde = ["gix/serde", "dep:serde_json", "dep:serde", "bytesize/serde"]
[dependencies]
# deselect everything else (like "performance") as this should be controllable by the parent application.
gix = { version = "^0.71.0", path = "../gix", default-features = false, features = ["merge", "blob-diff", "blame", "revision", "mailmap", "excludes", "attributes", "worktree-mutation", "credentials", "interrupt", "status", "dirwalk"] }
gix-date = { version = "^0.9.4", path = "../gix-date" }
gix-pack-for-configuration-only = { package = "gix-pack", version = "^0.58.0", path = "../gix-pack", default-features = false, features = ["pack-cache-lru-dynamic", "pack-cache-lru-static", "generate", "streaming-input"] }
gix-transport-configuration-only = { package = "gix-transport", version = "^0.46.0", path = "../gix-transport", default-features = false }
gix-archive-for-configuration-only = { package = "gix-archive", version = "^0.20.0", path = "../gix-archive", optional = true, features = ["tar", "tar_gz"] }
2 changes: 1 addition & 1 deletion gitoxide-core/src/hours/core.rs
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ pub fn estimate_hours(
let mut cur = commits.next().expect("at least one commit if we are here");

for next in commits {
let change_in_minutes = (next.time.seconds.saturating_sub(cur.time.seconds)) as f32 / MINUTES_PER_HOUR;
let change_in_minutes = (next.seconds().saturating_sub(cur.seconds())) as f32 / MINUTES_PER_HOUR;
if change_in_minutes < MAX_COMMIT_DIFFERENCE_IN_MINUTES {
hours += change_in_minutes / MINUTES_PER_HOUR;
} else {
13 changes: 4 additions & 9 deletions gitoxide-core/src/hours/mod.rs
Original file line number Diff line number Diff line change
@@ -82,22 +82,17 @@ where
};
let name = string_ref(author.name.as_ref());
let email = string_ref(author.email.as_ref());
let mut buf = Vec::with_capacity(64);
let time = string_ref(author.time.to_ref(&mut buf));

out.push((
commit_idx,
actor::SignatureRef {
name,
email,
time: author.time,
},
));
out.push((commit_idx, actor::SignatureRef { name, email, time }));
}
}
out.shrink_to_fit();
out.sort_by(|a, b| {
a.1.email
.cmp(b.1.email)
.then(a.1.time.seconds.cmp(&b.1.time.seconds).reverse())
.then(a.1.seconds().cmp(&b.1.seconds()).reverse())
});
Ok(out)
});
4 changes: 3 additions & 1 deletion gitoxide-core/src/query/engine/command.rs
Original file line number Diff line number Diff line change
@@ -76,7 +76,9 @@ impl query::Engine {
usize,
) = row?;
let id = gix::ObjectId::from(hash);
let commit_time = id.attach(&self.repo).object()?.into_commit().committer()?.time;
let commit_time = gix_date::Time::from_bytes(
id.attach(&self.repo).object()?.into_commit().committer()?.time,
)?;
let mode = FileMode::from_usize(mode).context("invalid file mode")?;
info.push(trace_path::Info {
id,
2 changes: 1 addition & 1 deletion gitoxide-core/src/repository/archive.rs
Original file line number Diff line number Diff line change
@@ -84,7 +84,7 @@ fn fetch_rev_info(
Ok(match object.kind {
gix::object::Kind::Commit => {
let commit = object.into_commit();
(Some(commit.committer()?.time.seconds), commit.tree_id()?.detach())
(Some(commit.committer()?.seconds()), commit.tree_id()?.detach())
}
gix::object::Kind::Tree => (None, object.id),
gix::object::Kind::Tag => fetch_rev_info(object.peel_to_kind(gix::object::Kind::Commit)?)?,
26 changes: 22 additions & 4 deletions gix-actor/src/lib.rs
Original file line number Diff line number Diff line change
@@ -13,12 +13,11 @@
///
/// For convenience to allow using `bstr` without adding it to own cargo manifest.
pub use bstr;
use bstr::{BStr, BString};
use bstr::{BStr, BString, ByteSlice};
/// The re-exported `gix-date` crate.
///
/// For convenience to allow using `gix-date` without adding it to own cargo manifest.
pub use gix_date as date;
use gix_date::Time;

mod identity;
///
@@ -68,9 +67,15 @@ pub struct Signature {
/// Use [SignatureRef::trim()] or trim manually to be able to clean it up.
pub email: BString,
/// The time stamp at which the signature is performed.
pub time: Time,
pub time: date::Time,
}

impl Signature {
/// Seconds since unix epoch from the time
pub fn seconds(&self) -> gix_date::SecondsSinceUnixEpoch {
self.time.seconds
}
}
/// A immutable signature is created by an actor at a certain time.
///
/// Note that this is not a cryptographical signature.
@@ -87,5 +92,18 @@ pub struct SignatureRef<'a> {
/// Use [SignatureRef::trim()] or trim manually to be able to clean it up.
pub email: &'a BStr,
/// The time stamp at which the signature was performed.
pub time: gix_date::Time,
pub time: &'a BStr,
}

impl SignatureRef<'_> {
/// Seconds since unix epoch from the time
pub fn seconds(&self) -> gix_date::SecondsSinceUnixEpoch {
use winnow::stream::AsChar;
self.time
.split(|b| b.is_space())
.next()
.and_then(|i| i.to_str().ok())
.and_then(|s| s.parse().ok())
.unwrap_or(Default::default())
}
}
68 changes: 17 additions & 51 deletions gix-actor/src/signature/decode.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
pub(crate) mod function {
use crate::{IdentityRef, SignatureRef};
use bstr::ByteSlice;
use gix_date::{time::Sign, OffsetInSeconds, SecondsSinceUnixEpoch, Time};
use gix_utils::btoi::to_signed;
use winnow::error::ErrMode;
use winnow::stream::Stream;
use winnow::{
combinator::{alt, opt, separated_pair, terminated},
combinator::{opt, separated_pair},
error::{AddContext, ParserError, StrContext},
prelude::*,
stream::AsChar,
token::{take, take_until, take_while},
token::take_while,
};

const SPACE: &[u8] = b" ";

/// Parse a signature from the bytes input `i` using `nom`.
pub fn decode<'a, E: ParserError<&'a [u8]> + AddContext<&'a [u8], StrContext>>(
i: &mut &'a [u8],
@@ -23,36 +19,13 @@ pub(crate) mod function {
identity,
opt(b" "),
opt((
terminated(take_until(0.., SPACE), take(1usize))
.verify_map(|v| to_signed::<SecondsSinceUnixEpoch>(v).ok())
.context(StrContext::Expected("<timestamp>".into())),
alt((
take_while(1.., b'-').map(|_| Sign::Minus),
take_while(1.., b'+').map(|_| Sign::Plus),
))
.context(StrContext::Expected("+|-".into())),
take_while(2, AsChar::is_dec_digit)
.verify_map(|v| to_signed::<OffsetInSeconds>(v).ok())
.context(StrContext::Expected("HH".into())),
take_while(1..=2, AsChar::is_dec_digit)
.verify_map(|v| to_signed::<OffsetInSeconds>(v).ok())
.context(StrContext::Expected("MM".into())),
take_while(0.., AsChar::is_dec_digit).map(|v: &[u8]| v),
take_while(0.., |b: u8| b == b'+' || b == b'-' || b.is_space() || b.is_dec_digit()).map(|v: &[u8]| v),
))
.map(|maybe_timestamp| {
if let Some((time, sign, hours, minutes, trailing_digits)) = maybe_timestamp {
let offset = if trailing_digits.is_empty() {
(hours * 3600 + minutes * 60) * if sign == Sign::Minus { -1 } else { 1 }
} else {
0
};
Time {
seconds: time,
offset,
sign,
}
.map(|maybe_bytes| {
if let Some((bytes,)) = maybe_bytes {
bytes.into()
} else {
Time::new(0, 0)
b"".into()
}
}),
)
@@ -109,29 +82,22 @@ pub use function::identity;
mod tests {
mod parse_signature {
use bstr::ByteSlice;
use gix_date::{time::Sign, OffsetInSeconds, SecondsSinceUnixEpoch};
use gix_testtools::to_bstr_err;
use winnow::prelude::*;

use crate::{signature, SignatureRef, Time};
use crate::{signature, SignatureRef};

fn decode<'i>(
i: &mut &'i [u8],
) -> ModalResult<SignatureRef<'i>, winnow::error::TreeError<&'i [u8], winnow::error::StrContext>> {
signature::decode.parse_next(i)
}

fn signature(
name: &'static str,
email: &'static str,
seconds: SecondsSinceUnixEpoch,
sign: Sign,
offset: OffsetInSeconds,
) -> SignatureRef<'static> {
fn signature(name: &'static str, email: &'static str, time: &'static str) -> SignatureRef<'static> {
SignatureRef {
name: name.as_bytes().as_bstr(),
email: email.as_bytes().as_bstr(),
time: Time { seconds, offset, sign },
time: time.as_bytes().as_bstr(),
}
}

@@ -142,7 +108,7 @@ mod tests {
.parse_peek(b"Sebastian Thiel <byronimo@gmail.com> 1528473343 -0230")
.expect("parse to work")
.1,
signature("Sebastian Thiel", "byronimo@gmail.com", 1528473343, Sign::Minus, -9000)
signature("Sebastian Thiel", "byronimo@gmail.com", "1528473343 -0230")
);
}

@@ -153,7 +119,7 @@ mod tests {
.parse_peek(b"Sebastian Thiel <byronimo@gmail.com> 1528473343 +0230")
.expect("parse to work")
.1,
signature("Sebastian Thiel", "byronimo@gmail.com", 1528473343, Sign::Plus, 9000)
signature("Sebastian Thiel", "byronimo@gmail.com", "1528473343 +0230")
);
}

@@ -164,7 +130,7 @@ mod tests {
.parse_peek(b"Sebastian Thiel <\tbyronimo@gmail.com > 1528473343 +0230")
.expect("parse to work")
.1,
signature("Sebastian Thiel", "\tbyronimo@gmail.com ", 1528473343, Sign::Plus, 9000)
signature("Sebastian Thiel", "\tbyronimo@gmail.com ", "1528473343 +0230")
);
}

@@ -175,7 +141,7 @@ mod tests {
.parse_peek(b"Sebastian Thiel <byronimo@gmail.com> 1528473343 -0000")
.expect("parse to work")
.1,
signature("Sebastian Thiel", "byronimo@gmail.com", 1528473343, Sign::Minus, 0)
signature("Sebastian Thiel", "byronimo@gmail.com", "1528473343 -0000")
);
}

@@ -186,15 +152,15 @@ mod tests {
.parse_peek(b"name <name@example.com> 1288373970 --700")
.expect("parse to work")
.1,
signature("name", "name@example.com", 1288373970, Sign::Minus, -252000)
signature("name", "name@example.com", "1288373970 --700")
);
}

#[test]
fn empty_name_and_email() {
assert_eq!(
decode.parse_peek(b" <> 12345 -1215").expect("parse to work").1,
signature("", "", 12345, Sign::Minus, -44100)
signature("", "", "12345 -1215")
);
}

@@ -213,7 +179,7 @@ mod tests {
fn invalid_time() {
assert_eq!(
decode.parse_peek(b"hello <> abc -1215").expect("parse to work").1,
signature("hello", "", 0, Sign::Plus, 0)
signature("hello", "", "")
);
}
}
27 changes: 12 additions & 15 deletions gix-actor/src/signature/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod _ref {
use bstr::ByteSlice;
use gix_date::Time;
use winnow::{error::StrContext, prelude::*};

use crate::{signature::decode, IdentityRef, Signature, SignatureRef};
@@ -18,7 +19,7 @@ mod _ref {
Signature {
name: self.name.to_owned(),
email: self.email.to_owned(),
time: self.time,
time: Time::from_bytes(self.time).expect("Time must be valid"),
}
}

@@ -27,7 +28,7 @@ mod _ref {
SignatureRef {
name: self.name.trim().as_bstr(),
email: self.email.trim().as_bstr(),
time: self.time,
time: self.time.trim().as_bstr(),
}
}

@@ -43,14 +44,15 @@ mod _ref {

mod convert {
use crate::{Signature, SignatureRef};
use gix_date::Time;

impl Signature {
/// Borrow this instance as immutable
pub fn to_ref(&self) -> SignatureRef<'_> {
pub fn to_ref<'a>(&'a self, buf: &'a mut Vec<u8>) -> SignatureRef<'a> {
SignatureRef {
name: self.name.as_ref(),
email: self.email.as_ref(),
time: self.time,
time: self.time.to_ref(buf),
}
}
}
@@ -61,16 +63,10 @@ mod convert {
Signature {
name: name.to_owned(),
email: email.to_owned(),
time,
time: Time::from_bytes(time).expect("Time must be valid"),
}
}
}

impl<'a> From<&'a Signature> for SignatureRef<'a> {
fn from(other: &'a Signature) -> SignatureRef<'a> {
other.to_ref()
}
}
}

pub(crate) mod write {
@@ -96,11 +92,12 @@ pub(crate) mod write {
impl Signature {
/// Serialize this instance to `out` in the git serialization format for actors.
pub fn write_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> {
self.to_ref().write_to(out)
let mut buf = Vec::<u8>::new();
self.to_ref(&mut buf).write_to(out)
}
/// Computes the number of bytes necessary to serialize this signature
pub fn size(&self) -> usize {
self.to_ref().size()
self.name.len() + 2 /* space <*/ + self.email.len() + 2 /* > space */ + self.time.size()
}
}

@@ -112,11 +109,11 @@ pub(crate) mod write {
out.write_all(b"<")?;
out.write_all(validated_token(self.email)?)?;
out.write_all(b"> ")?;
self.time.write_to(out)
out.write_all(validated_token(self.time)?)
}
/// Computes the number of bytes necessary to serialize this signature
pub fn size(&self) -> usize {
self.name.len() + 2 /* space <*/ + self.email.len() + 2 /* > space */ + self.time.size()
self.name.len() + 2 /* space <*/ + self.email.len() + 2 /* > space */ + self.time.len()
}
}

Loading