use std::path::{Component, Path};
use serde::{Deserialize, Serialize};
use crate::{
error::HeddleError,
object::{ContentHash, Origin, State, StateId},
util::{BudgetExceeded, LineDiffError, ResourceUsage},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlameSliceLimits {
pub states: u64,
pub decoded_bytes: u64,
pub lines: u64,
pub diff_work: u64,
pub scratch_bytes: u64,
}
impl BlameSliceLimits {
pub fn unlimited() -> Self {
Self {
states: u64::MAX,
decoded_bytes: u64::MAX,
lines: u64::MAX,
diff_work: u64::MAX,
scratch_bytes: u64::MAX,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlameLineMap {
pub state_start: u32,
pub target_start: u32,
pub len: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlameTarget {
pub blob: ContentHash,
pub line_count: u32,
pub state_id: StateId,
pub path: String,
}
impl BlameTarget {
pub fn bind(
state_id: StateId,
path: &Path,
blob: ContentHash,
line_count: u32,
) -> Result<Self, BlameSliceError> {
Ok(Self {
blob,
line_count,
state_id,
path: normalize_blame_path(path)?,
})
}
pub fn matches_path(&self, path: &Path) -> Result<bool, BlameSliceError> {
Ok(self.path == normalize_blame_path(path)?)
}
}
pub(super) fn normalize_blame_path(path: &Path) -> Result<String, BlameSliceError> {
let mut parts = Vec::new();
for component in path.components() {
match component {
Component::Normal(name) => {
let Some(name) = name.to_str() else {
return Err(BlameSliceError::InvalidFrontier(
"target path is not valid UTF-8".into(),
));
};
parts.push(name);
}
Component::CurDir => {}
_ => {
return Err(BlameSliceError::InvalidFrontier(
"target path is not a normalized repo path".into(),
));
}
}
}
if parts.is_empty() {
return Err(BlameSliceError::InvalidFrontier(
"target path is empty".into(),
));
}
Ok(parts.join("/"))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlameFrontierRecord {
pub origin: Origin,
pub blob_hash: ContentHash,
pub state_line_count: u32,
pub mappings: Vec<BlameLineMap>,
pub target: BlameTarget,
}
impl BlameFrontierRecord {
pub fn state_id(&self) -> StateId {
self.origin.state_id
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlameFrontierGroup {
pub target: BlameTarget,
pub records: Vec<BlameFrontierRecord>,
}
impl BlameFrontierGroup {
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
pub fn pop(&mut self) -> Option<BlameFrontierRecord> {
self.records.pop()
}
pub fn push(&mut self, record: BlameFrontierRecord) {
self.records.push(record);
}
pub fn require_target(&self, expected: &BlameTarget) -> Result<(), BlameSliceError> {
if &self.target != expected {
return Err(BlameSliceError::InvalidFrontier(
"frontier target does not match prepared target".into(),
));
}
self.require_consistent_target()
}
pub fn require_path(&self, path: &Path) -> Result<(), BlameSliceError> {
if !self.target.matches_path(path)? {
return Err(BlameSliceError::InvalidFrontier(
"frontier target path does not match advance path".into(),
));
}
Ok(())
}
pub fn require_consistent_target(&self) -> Result<(), BlameSliceError> {
if self
.records
.iter()
.any(|record| record.target != self.target)
{
return Err(BlameSliceError::InvalidFrontier(
"frontier record target does not match group".into(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OriginRange {
pub target_start: u32,
pub len: u32,
pub origin: Origin,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BlamePreparation {
MissingPath,
Unblamable,
Empty {
file_blob: ContentHash,
origin: Origin,
},
Active {
file_blob: ContentHash,
line_count: u32,
frontier: BlameFrontierGroup,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BlameSliceAdvance {
Progress {
next: BlameFrontierGroup,
finalized: Vec<OriginRange>,
usage: ResourceUsage,
},
Complete {
finalized: Vec<OriginRange>,
usage: ResourceUsage,
},
}
#[derive(Debug, thiserror::Error)]
pub enum BlameSliceError {
#[error("path is absent from the target state")]
MissingPath,
#[error("file is binary or otherwise unblamable")]
Unblamable,
#[error("missing {kind} {id}")]
MissingObject { kind: &'static str, id: String },
#[error(transparent)]
BudgetExceeded(#[from] BudgetExceeded),
#[error("invalid frontier: {0}")]
InvalidFrontier(String),
#[error("invalid origin coverage")]
InvalidCoverage,
#[error(transparent)]
Store(#[from] HeddleError),
}
impl From<LineDiffError> for BlameSliceError {
fn from(error: LineDiffError) -> Self {
match error {
LineDiffError::InvalidUtf8 => Self::Unblamable,
LineDiffError::BudgetExceeded(error) => Self::BudgetExceeded(error),
LineDiffError::Visitor(never) => match never {},
}
}
}
pub fn origin_from_state(state: &State) -> Origin {
Origin {
state_id: state.id(),
attribution: state.attribution.clone(),
created_at: state.created_at,
authored_at: state.authored_at,
}
}