1use std::path::{Component, Path};
5
6use serde::{Deserialize, Serialize};
7
8use crate::{
9 error::HeddleError,
10 object::{ContentHash, Origin, State, StateId},
11 util::{BudgetExceeded, LineDiffError, ResourceUsage},
12};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub struct BlameSliceLimits {
17 pub states: u64,
18 pub decoded_bytes: u64,
19 pub lines: u64,
20 pub diff_work: u64,
21 pub scratch_bytes: u64,
22}
23
24impl BlameSliceLimits {
25 pub fn unlimited() -> Self {
26 Self {
27 states: u64::MAX,
28 decoded_bytes: u64::MAX,
29 lines: u64::MAX,
30 diff_work: u64::MAX,
31 scratch_bytes: u64::MAX,
32 }
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38pub struct BlameLineMap {
39 pub state_start: u32,
40 pub target_start: u32,
41 pub len: u32,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49pub struct BlameTarget {
50 pub blob: ContentHash,
51 pub line_count: u32,
52 pub state_id: StateId,
53 pub path: String,
54}
55
56impl BlameTarget {
57 pub fn bind(
59 state_id: StateId,
60 path: &Path,
61 blob: ContentHash,
62 line_count: u32,
63 ) -> Result<Self, BlameSliceError> {
64 Ok(Self {
65 blob,
66 line_count,
67 state_id,
68 path: normalize_blame_path(path)?,
69 })
70 }
71
72 pub fn matches_path(&self, path: &Path) -> Result<bool, BlameSliceError> {
73 Ok(self.path == normalize_blame_path(path)?)
74 }
75}
76
77pub(super) fn normalize_blame_path(path: &Path) -> Result<String, BlameSliceError> {
80 let mut parts = Vec::new();
81 for component in path.components() {
82 match component {
83 Component::Normal(name) => {
84 let Some(name) = name.to_str() else {
85 return Err(BlameSliceError::InvalidFrontier(
86 "target path is not valid UTF-8".into(),
87 ));
88 };
89 parts.push(name);
90 }
91 Component::CurDir => {}
92 _ => {
93 return Err(BlameSliceError::InvalidFrontier(
94 "target path is not a normalized repo path".into(),
95 ));
96 }
97 }
98 }
99 if parts.is_empty() {
100 return Err(BlameSliceError::InvalidFrontier(
101 "target path is empty".into(),
102 ));
103 }
104 Ok(parts.join("/"))
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109pub struct BlameFrontierRecord {
110 pub origin: Origin,
111 pub blob_hash: ContentHash,
112 pub state_line_count: u32,
113 pub mappings: Vec<BlameLineMap>,
114 pub target: BlameTarget,
115}
116
117impl BlameFrontierRecord {
118 pub fn state_id(&self) -> StateId {
119 self.origin.state_id
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128pub struct BlameFrontierGroup {
129 pub target: BlameTarget,
130 pub records: Vec<BlameFrontierRecord>,
131}
132
133impl BlameFrontierGroup {
134 pub fn is_empty(&self) -> bool {
135 self.records.is_empty()
136 }
137
138 pub fn pop(&mut self) -> Option<BlameFrontierRecord> {
139 self.records.pop()
140 }
141
142 pub fn push(&mut self, record: BlameFrontierRecord) {
143 self.records.push(record);
144 }
145
146 pub fn require_target(&self, expected: &BlameTarget) -> Result<(), BlameSliceError> {
148 if &self.target != expected {
149 return Err(BlameSliceError::InvalidFrontier(
150 "frontier target does not match prepared target".into(),
151 ));
152 }
153 self.require_consistent_target()
154 }
155
156 pub fn require_path(&self, path: &Path) -> Result<(), BlameSliceError> {
158 if !self.target.matches_path(path)? {
159 return Err(BlameSliceError::InvalidFrontier(
160 "frontier target path does not match advance path".into(),
161 ));
162 }
163 Ok(())
164 }
165
166 pub fn require_consistent_target(&self) -> Result<(), BlameSliceError> {
167 if self
168 .records
169 .iter()
170 .any(|record| record.target != self.target)
171 {
172 return Err(BlameSliceError::InvalidFrontier(
173 "frontier record target does not match group".into(),
174 ));
175 }
176 Ok(())
177 }
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
182pub struct OriginRange {
183 pub target_start: u32,
184 pub len: u32,
185 pub origin: Origin,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
190pub enum BlamePreparation {
191 MissingPath,
192 Unblamable,
193 Empty {
194 file_blob: ContentHash,
195 origin: Origin,
196 },
197 Active {
198 file_blob: ContentHash,
199 line_count: u32,
200 frontier: BlameFrontierGroup,
201 },
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
206pub enum BlameSliceAdvance {
207 Progress {
208 next: BlameFrontierGroup,
209 finalized: Vec<OriginRange>,
210 usage: ResourceUsage,
211 },
212 Complete {
213 finalized: Vec<OriginRange>,
214 usage: ResourceUsage,
215 },
216}
217
218#[derive(Debug, thiserror::Error)]
220pub enum BlameSliceError {
221 #[error("path is absent from the target state")]
222 MissingPath,
223 #[error("file is binary or otherwise unblamable")]
224 Unblamable,
225 #[error("missing {kind} {id}")]
226 MissingObject { kind: &'static str, id: String },
227 #[error(transparent)]
228 BudgetExceeded(#[from] BudgetExceeded),
229 #[error("invalid frontier: {0}")]
230 InvalidFrontier(String),
231 #[error("invalid origin coverage")]
232 InvalidCoverage,
233 #[error(transparent)]
234 Store(#[from] HeddleError),
235}
236
237impl From<LineDiffError> for BlameSliceError {
238 fn from(error: LineDiffError) -> Self {
239 match error {
240 LineDiffError::InvalidUtf8 => Self::Unblamable,
241 LineDiffError::BudgetExceeded(error) => Self::BudgetExceeded(error),
242 LineDiffError::Visitor(never) => match never {},
243 }
244 }
245}
246
247pub fn origin_from_state(state: &State) -> Origin {
248 Origin {
249 state_id: state.id(),
250 attribution: state.attribution.clone(),
251 created_at: state.created_at,
252 authored_at: state.authored_at,
253 }
254}