use crate::refs::reftable::LogValue;
use crate::{IdentityRef, ObjectFormat, ObjectId};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ImportedRecord {
File {
format: ObjectFormat,
bytes: Vec<u8>,
},
Reftable {
update_index: u64,
value: LogValue,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ReflogFields<'a> {
pub old: ObjectId,
pub new: ObjectId,
pub name: &'a [u8],
pub email: &'a [u8],
pub seconds: i128,
pub offset_minutes: i16,
pub message: &'a [u8],
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
pub enum ReflogInterpretationError {
#[error("incomplete reflog record")]
Incomplete,
#[error("malformed reflog object IDs")]
ObjectIds,
#[error("malformed reflog identity or date")]
Identity,
#[error("reflog date exceeds interpretation range")]
DateRange,
}
impl ImportedRecord {
pub fn recoverable_roots(&self) -> impl Iterator<Item = ObjectId> {
let ids = match self {
Self::File { format, bytes } => file_ids(*format, bytes),
Self::Reftable { value, .. } => [Some(value.old), Some(value.new)],
};
ids.into_iter()
.flatten()
.filter(|id| *id != ObjectId::null(id.format()))
}
pub fn fields(&self) -> Result<ReflogFields<'_>, ReflogInterpretationError> {
match self {
Self::File { format, bytes } => file_fields(*format, bytes),
Self::Reftable { value, .. } => Ok(ReflogFields {
old: value.old,
new: value.new,
name: &value.name,
email: &value.email,
seconds: i128::from(value.seconds),
offset_minutes: value.offset_minutes,
message: value.message.strip_suffix(b"\n").unwrap_or(&value.message),
}),
}
}
}
fn file_ids(format: ObjectFormat, bytes: &[u8]) -> [Option<ObjectId>; 2] {
let width = format.digest_len() * 2;
let id = |start| {
bytes
.get(start..start + width)
.and_then(|s| std::str::from_utf8(s).ok())
.and_then(|s| ObjectId::from_hex(format, s).ok())
};
[id(0), id(width + 1)]
}
fn file_fields(
format: ObjectFormat,
bytes: &[u8],
) -> Result<ReflogFields<'_>, ReflogInterpretationError> {
use ReflogInterpretationError as E;
let line = bytes.strip_suffix(b"\n").ok_or(E::Incomplete)?;
let width = format.digest_len() * 2;
let [Some(old), Some(new)] = file_ids(format, line) else {
return Err(E::ObjectIds);
};
if line.get(width) != Some(&b' ') || line.get(2 * width + 1) != Some(&b' ') {
return Err(E::ObjectIds);
}
let identity = IdentityRef::parse(&line[2 * width + 2..]).map_err(|_| E::Identity)?;
let date = identity.date_bytes.strip_prefix(b" ").ok_or(E::Identity)?;
let split = date.iter().position(|b| *b == b' ').ok_or(E::Identity)?;
let seconds = std::str::from_utf8(&date[..split]).map_err(|_| E::Identity)?;
let digits = seconds.strip_prefix(['+', '-']).unwrap_or(seconds);
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return Err(E::Identity);
}
let seconds = seconds.parse::<i128>().map_err(|_| E::DateRange)?;
let zone = &date[split + 1..];
if zone.len() < 5
|| !matches!(zone[0], b'+' | b'-')
|| !zone[1..5].iter().all(u8::is_ascii_digit)
{
return Err(E::Identity);
}
let hhmm = std::str::from_utf8(&zone[..5])
.unwrap()
.parse::<i16>()
.unwrap();
let message = zone[5..].strip_prefix(b"\t").unwrap_or(&zone[5..]);
Ok(ReflogFields {
old,
new,
name: identity.name,
email: identity.email,
seconds,
offset_minutes: hhmm / 100 * 60 + hhmm % 100,
message,
})
}