use crate::types::{
BlameError, BlameLine, BlameOptions, BlameResult, BlameRevision, DiffAlgorithm,
};
use similar::{Algorithm, ChangeTag, capture_diff_slices};
use std::rc::Rc;
#[derive(Debug)]
struct LineOrigin<'a, T> {
content: &'a str,
metadata: Rc<T>,
}
impl<'a, T> Clone for LineOrigin<'a, T> {
fn clone(&self) -> Self {
Self {
content: self.content,
metadata: Rc::clone(&self.metadata),
}
}
}
fn iter_lines_preserve_terminator(content: &str) -> impl Iterator<Item = &str> {
content.split_inclusive('\n')
}
pub fn blame<'a, T>(
revisions: &'a [BlameRevision<'a, T>],
) -> Result<BlameResult<'a, T>, BlameError> {
blame_with_options(revisions, BlameOptions::default())
}
pub fn blame_with_options<'a, T>(
revisions: &'a [BlameRevision<'a, T>],
options: BlameOptions,
) -> Result<BlameResult<'a, T>, BlameError> {
if revisions.is_empty() {
return Err(BlameError::EmptyRevisions);
}
let similar_algorithm = match options.algorithm {
DiffAlgorithm::Myers => Algorithm::Myers,
DiffAlgorithm::Patience => Algorithm::Patience,
};
let revision_lines: Vec<Vec<&'a str>> = revisions
.iter()
.map(|revision| iter_lines_preserve_terminator(revision.content).collect())
.collect();
let mut line_origins: Vec<LineOrigin<'a, T>> = Vec::with_capacity(revision_lines[0].len());
let first_metadata = Rc::clone(&revisions[0].metadata);
for &line in &revision_lines[0] {
line_origins.push(LineOrigin {
content: line,
metadata: Rc::clone(&first_metadata),
});
}
for i in 0..revisions.len() - 1 {
let old_lines = &revision_lines[i];
let new_lines = &revision_lines[i + 1];
let shared_metadata = Rc::clone(&revisions[i + 1].metadata);
let diff_ops = capture_diff_slices(similar_algorithm, &old_lines, &new_lines);
let mut new_line_origins: Vec<LineOrigin<'a, T>> = Vec::with_capacity(new_lines.len());
for op in &diff_ops {
for change in op.iter_changes(old_lines, new_lines) {
match change.tag() {
ChangeTag::Equal => {
let old_line_num = change.old_index().ok_or_else(|| {
BlameError::InvalidInput(format!(
"diff invariant violated: Equal change had no old index at revision {}",
i + 1
))
})?;
let origin = line_origins.get(old_line_num).ok_or_else(|| {
BlameError::InvalidInput(format!(
"diff invariant violated: old index {} out of bounds (len {}) at revision {}",
old_line_num,
line_origins.len(),
i + 1
))
})?;
new_line_origins.push(origin.clone());
}
ChangeTag::Insert => {
new_line_origins.push(LineOrigin {
content: change.value(),
metadata: Rc::clone(&shared_metadata),
});
}
ChangeTag::Delete => {}
}
}
}
line_origins = new_line_origins;
}
let blame_lines: Vec<BlameLine<'a, T>> = line_origins
.into_iter()
.enumerate()
.map(|(idx, origin)| BlameLine {
line_number: idx,
content: origin.content,
revision_metadata: origin.metadata,
})
.collect();
Ok(BlameResult::new(blame_lines))
}