use similar::{ChangeTag, TextDiff};
use crate::backend::{
models::CommentAnchor,
proto::lgtm::v1::{DiffHunkDetail, DiffLineDetail, DiffLineKind as ProtoDiffLineKind},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DiffLineKind {
Context,
Added,
Removed,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DiffLine {
pub kind: DiffLineKind,
pub line_number: u32,
pub content: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DiffHunk {
pub file_path: String,
pub commit_sha: String,
pub start_line: u32,
pub lines: Vec<DiffLine>,
}
impl DiffHunk {
pub fn from_blobs(
file_path: &str,
commit_sha: &str,
base_blob: Option<&str>,
head_blob: Option<&str>,
anchors: &[CommentAnchor],
) -> Vec<Self> {
let old_text = base_blob.unwrap_or("");
let new_text = head_blob.unwrap_or("");
let diff = TextDiff::from_lines(old_text, new_text);
let mut hunks = Vec::new();
for anchor in anchors {
if anchor.file_path != file_path {
continue;
}
let window_start = anchor
.start_line
.unwrap_or(anchor.line)
.saturating_sub(3)
.max(1);
let window_end = anchor.line.saturating_add(3);
let mut lines = Vec::new();
let mut new_line_cursor: u32 = 1;
for change in diff.iter_all_changes() {
let content = change
.value()
.trim_end_matches(&['\r', '\n'][..])
.to_owned();
match change.tag() {
ChangeTag::Equal => {
if let Some(new_idx) = change.new_index() {
let line_num = u32::try_from(new_idx + 1).unwrap_or(1);
new_line_cursor = line_num.saturating_add(1);
if line_num >= window_start && line_num <= window_end {
lines.push(DiffLine {
kind: DiffLineKind::Context,
line_number: line_num,
content,
});
}
}
}
ChangeTag::Insert => {
if let Some(new_idx) = change.new_index() {
let line_num = u32::try_from(new_idx + 1).unwrap_or(1);
new_line_cursor = line_num.saturating_add(1);
if line_num >= window_start && line_num <= window_end {
lines.push(DiffLine {
kind: DiffLineKind::Added,
line_number: line_num,
content,
});
}
}
}
ChangeTag::Delete => {
if let Some(old_idx) = change.old_index() {
let line_num = u32::try_from(old_idx + 1).unwrap_or(1);
if new_line_cursor >= window_start && new_line_cursor <= window_end {
lines.push(DiffLine {
kind: DiffLineKind::Removed,
line_number: line_num,
content,
});
}
}
}
}
}
let start_line = lines
.first()
.map(|l| l.line_number)
.unwrap_or(anchor.start_line.unwrap_or(anchor.line));
hunks.push(Self {
file_path: anchor.file_path.clone(),
commit_sha: commit_sha.to_owned(),
start_line,
lines,
});
}
hunks
}
}
impl From<DiffLineKind> for ProtoDiffLineKind {
fn from(kind: DiffLineKind) -> Self {
match kind {
DiffLineKind::Context => Self::DIFF_LINE_KIND_CONTEXT,
DiffLineKind::Added => Self::DIFF_LINE_KIND_ADDED,
DiffLineKind::Removed => Self::DIFF_LINE_KIND_REMOVED,
}
}
}
impl From<DiffLine> for DiffLineDetail {
fn from(line: DiffLine) -> Self {
Self {
kind: ProtoDiffLineKind::from(line.kind).into(),
line_number: line.line_number,
content: line.content,
..Default::default()
}
}
}
impl From<DiffHunk> for DiffHunkDetail {
fn from(hunk: DiffHunk) -> Self {
Self {
file_path: hunk.file_path,
commit_sha: hunk.commit_sha,
start_line: hunk.start_line,
lines: hunk.lines.into_iter().map(DiffLineDetail::from).collect(),
..Default::default()
}
}
}