gitdataai/libs/git/diff/ops.rs
2026-04-14 19:02:01 +08:00

511 lines
17 KiB
Rust

//! Diff operations.
use std::cell::RefCell;
use crate::commit::types::CommitOid;
use crate::diff::types::{
DiffDelta, DiffHunk, DiffLine, DiffOptions, DiffResult, DiffStats, SideBySideChangeType,
SideBySideDiffResult, SideBySideFile, SideBySideLine,
};
use crate::{GitDomain, GitError, GitResult};
impl GitDomain {
pub fn diff_tree_to_tree(
&self,
old_tree: Option<&CommitOid>,
new_tree: Option<&CommitOid>,
opts: Option<DiffOptions>,
) -> GitResult<DiffResult> {
let old_tree = match old_tree {
Some(oid) => {
let o = oid
.to_oid()
.map_err(|_| GitError::InvalidOid(oid.to_string()))?;
Some(
self.repo()
.find_tree(o)
.map_err(|e| GitError::Internal(e.to_string()))?,
)
}
None => None,
};
let new_tree = match new_tree {
Some(oid) => {
let o = oid
.to_oid()
.map_err(|_| GitError::InvalidOid(oid.to_string()))?;
Some(
self.repo()
.find_tree(o)
.map_err(|e| GitError::Internal(e.to_string()))?,
)
}
None => None,
};
let mut git_opts = opts
.map(|o| o.to_git2())
.unwrap_or_else(git2::DiffOptions::new);
// git2 requires at least one tree to be Some
let diff = match (old_tree.as_ref(), new_tree.as_ref()) {
(Some(old), Some(new)) => {
self.repo()
.diff_tree_to_tree(Some(old), Some(new), Some(&mut git_opts))
}
(Some(old), None) => {
self.repo()
.diff_tree_to_tree(Some(old), None, Some(&mut git_opts))
}
(None, Some(new)) => {
self.repo()
.diff_tree_to_tree(None, Some(new), Some(&mut git_opts))
}
(None, None) => {
return Err(GitError::Internal(
"Both old_tree and new_tree are None".into(),
));
}
}
.map_err(|e| GitError::Internal(e.to_string()))?;
build_diff_result(&diff)
}
pub fn diff_commit_to_workdir(
&self,
commit: &CommitOid,
opts: Option<DiffOptions>,
) -> GitResult<DiffResult> {
let oid = commit
.to_oid()
.map_err(|_| GitError::InvalidOid(commit.to_string()))?;
let commit = self
.repo()
.find_commit(oid)
.map_err(|e| GitError::Internal(e.to_string()))?;
let tree = self
.repo()
.find_tree(commit.tree_id())
.map_err(|e| GitError::Internal(e.to_string()))?;
let mut git_opts = opts
.map(|o| o.to_git2())
.unwrap_or_else(git2::DiffOptions::new);
let diff = self
.repo()
.diff_tree_to_workdir(Some(&tree), Some(&mut git_opts))
.map_err(|e| GitError::Internal(e.to_string()))?;
build_diff_result(&diff)
}
pub fn diff_commit_to_index(
&self,
commit: &CommitOid,
opts: Option<DiffOptions>,
) -> GitResult<DiffResult> {
let oid = commit
.to_oid()
.map_err(|_| GitError::InvalidOid(commit.to_string()))?;
let commit = self
.repo()
.find_commit(oid)
.map_err(|e| GitError::Internal(e.to_string()))?;
let tree = self
.repo()
.find_tree(commit.tree_id())
.map_err(|e| GitError::Internal(e.to_string()))?;
// Get the index as a tree for comparison.
let mut index = self
.repo()
.index()
.map_err(|e| GitError::Internal(e.to_string()))?;
let index_tree_oid = index
.write_tree()
.map_err(|e| GitError::Internal(e.to_string()))?;
let index_tree = self
.repo()
.find_tree(index_tree_oid)
.map_err(|e| GitError::Internal(e.to_string()))?;
let mut git_opts = opts
.map(|o| o.to_git2())
.unwrap_or_else(git2::DiffOptions::new);
let diff = self
.repo()
.diff_tree_to_tree(Some(&tree), Some(&index_tree), Some(&mut git_opts))
.map_err(|e| GitError::Internal(e.to_string()))?;
build_diff_result(&diff)
}
pub fn diff_workdir_to_index(&self, opts: Option<DiffOptions>) -> GitResult<DiffResult> {
let mut git_opts = opts
.map(|o| o.to_git2())
.unwrap_or_else(git2::DiffOptions::new);
let diff = self
.repo()
.diff_tree_to_workdir(None, Some(&mut git_opts))
.map_err(|e| GitError::Internal(e.to_string()))?;
build_diff_result(&diff)
}
pub fn diff_index_to_tree(
&self,
tree: &CommitOid,
opts: Option<DiffOptions>,
) -> GitResult<DiffResult> {
let oid = tree
.to_oid()
.map_err(|_| GitError::InvalidOid(tree.to_string()))?;
let tree = self
.repo()
.find_tree(oid)
.map_err(|e| GitError::Internal(e.to_string()))?;
let mut git_opts = opts
.map(|o| o.to_git2())
.unwrap_or_else(git2::DiffOptions::new);
let diff = self
.repo()
.diff_tree_to_tree(Some(&tree), None, Some(&mut git_opts))
.map_err(|e| GitError::Internal(e.to_string()))?;
build_diff_result(&diff)
}
pub fn diff_stats(&self, old_tree: &CommitOid, new_tree: &CommitOid) -> GitResult<DiffStats> {
let old_oid = old_tree
.to_oid()
.map_err(|_| GitError::InvalidOid(old_tree.to_string()))?;
let new_oid = new_tree
.to_oid()
.map_err(|_| GitError::InvalidOid(new_tree.to_string()))?;
let old_tree = self
.repo()
.find_tree(old_oid)
.map_err(|e| GitError::Internal(e.to_string()))?;
let new_tree = self
.repo()
.find_tree(new_oid)
.map_err(|e| GitError::Internal(e.to_string()))?;
let diff = self
.repo()
.diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None)
.map_err(|e| GitError::Internal(e.to_string()))?;
let stats = diff
.stats()
.map_err(|e| GitError::Internal(e.to_string()))?;
Ok(DiffStats::from_git2(&stats))
}
pub fn diff_patch_id(&self, old_tree: &CommitOid, new_tree: &CommitOid) -> GitResult<String> {
let old_oid = old_tree
.to_oid()
.map_err(|_| GitError::InvalidOid(old_tree.to_string()))?;
let new_oid = new_tree
.to_oid()
.map_err(|_| GitError::InvalidOid(new_tree.to_string()))?;
let old_tree = self
.repo()
.find_tree(old_oid)
.map_err(|e| GitError::Internal(e.to_string()))?;
let new_tree = self
.repo()
.find_tree(new_oid)
.map_err(|e| GitError::Internal(e.to_string()))?;
let diff = self
.repo()
.diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None)
.map_err(|e| GitError::Internal(e.to_string()))?;
let patch_id = diff
.patchid(None)
.map_err(|e| GitError::Internal(e.to_string()))?;
Ok(patch_id.to_string())
}
}
fn build_diff_result(diff: &git2::Diff<'_>) -> GitResult<DiffResult> {
let stats = diff
.stats()
.map_err(|e| GitError::Internal(e.to_string()))?;
let delta_count = diff.deltas().len();
let deltas: RefCell<Vec<DiffDelta>> = RefCell::new(Vec::with_capacity(delta_count));
let delta_hunks: RefCell<Vec<DiffHunk>> = RefCell::new(Vec::new());
let delta_lines: RefCell<Vec<DiffLine>> = RefCell::new(Vec::new());
let counter: RefCell<usize> = RefCell::new(0);
let mut file_cb = |_delta: git2::DiffDelta<'_>, _progress: f32| -> bool {
let count = *counter.borrow();
if count > 0 {
let prev_idx = count - 1;
let hunks = delta_hunks.take();
let lines = delta_lines.take();
if let Some(prev_delta) = diff.get_delta(prev_idx) {
deltas
.borrow_mut()
.push(DiffDelta::from_git2(&prev_delta, hunks, lines));
}
}
*counter.borrow_mut() = count + 1;
true
};
let mut hunk_cb = |_delta: git2::DiffDelta<'_>, hunk: git2::DiffHunk<'_>| -> bool {
delta_hunks.borrow_mut().push(DiffHunk::from_git2(&hunk));
true
};
let mut line_cb = |_delta: git2::DiffDelta<'_>,
_hunk: Option<git2::DiffHunk<'_>>,
line: git2::DiffLine<'_>|
-> bool {
delta_lines.borrow_mut().push(DiffLine::from_git2(&line));
true
};
diff.foreach(&mut file_cb, None, Some(&mut hunk_cb), Some(&mut line_cb))
.map_err(|e| GitError::Internal(e.to_string()))?;
{
let count = *counter.borrow();
if count > 0 {
let last_idx = count - 1;
let hunks = delta_hunks.take();
let lines = delta_lines.take();
if let Some(last_delta) = diff.get_delta(last_idx) {
deltas
.borrow_mut()
.push(DiffDelta::from_git2(&last_delta, hunks, lines));
}
}
}
Ok(DiffResult {
stats: DiffStats::from_git2(&stats),
deltas: deltas.into_inner(),
})
}
/// The algorithm walks the unified diff line-by-line and produces rows where:
/// - **Added** lines appear only on the right side.
/// - **Deleted** lines appear only on the left side.
/// - When a deletion is immediately followed by an addition they are rendered as a
/// **Modified** pair (two separate rows).
/// - **Context** lines appear on both sides with the same content.
/// - **Empty** filler rows are inserted so that left and right line numbers stay aligned.
pub fn diff_to_side_by_side(diff: &DiffResult) -> SideBySideDiffResult {
let mut files: Vec<SideBySideFile> = Vec::with_capacity(diff.deltas.len());
let mut total_additions = 0usize;
let mut total_deletions = 0usize;
for delta in &diff.deltas {
let (path, is_binary, is_rename) =
if delta.status == crate::diff::types::DiffDeltaStatus::Renamed {
(
delta
.new_file
.path
.clone()
.or_else(|| delta.old_file.path.clone())
.unwrap_or_default(),
delta.new_file.is_binary || delta.old_file.is_binary,
true,
)
} else {
(
delta
.new_file
.path
.clone()
.or_else(|| delta.old_file.path.clone())
.unwrap_or_default(),
delta.new_file.is_binary,
false,
)
};
if is_binary {
files.push(SideBySideFile {
path,
additions: 0,
deletions: 0,
is_binary: true,
is_rename,
lines: vec![],
});
continue;
}
let lines = build_side_by_side_lines(&delta.lines);
let additions = lines
.iter()
.filter(|l| matches!(l.change_type, SideBySideChangeType::Added))
.count();
let deletions = lines
.iter()
.filter(|l| matches!(l.change_type, SideBySideChangeType::Removed))
.count();
total_additions += additions;
total_deletions += deletions;
files.push(SideBySideFile {
path,
additions,
deletions,
is_binary: false,
is_rename,
lines,
});
}
SideBySideDiffResult {
files,
total_additions,
total_deletions,
}
}
fn build_side_by_side_lines(unified: &[DiffLine]) -> Vec<SideBySideLine> {
let mut result: Vec<SideBySideLine> = Vec::with_capacity(unified.len() * 2);
let mut i = 0;
while i < unified.len() {
let line = &unified[i];
match line.origin {
'+' => {
// Collect a run of consecutive additions.
let mut added_lines: Vec<&DiffLine> = vec![line];
while i + 1 < unified.len() && unified[i + 1].origin == '+' {
i += 1;
added_lines.push(&unified[i]);
}
// Peek at the previous deletion run to pair with additions.
let mut deleted_lines: Vec<&DiffLine> = vec![];
// Backtrack to find deletions right before this run.
let Some(j_base) = i.checked_sub(added_lines.len()) else {
// Additions at start of diff — no preceding deletions to pair with.
// Emit all as unpaired Added below.
for k in 0..added_lines.len() {
let add = added_lines[k];
result.push(SideBySideLine {
left_line_no: None,
right_line_no: add.new_lineno,
left_content: String::new(),
right_content: add.content.clone(),
change_type: SideBySideChangeType::Added,
});
}
i += 1;
continue;
};
let mut j = j_base;
while j > 0 && unified[j].origin == '-' {
j -= 1;
}
// Collect deletions from j+1 up to (but not including) the first addition.
let del_start = if j > 0 { j + 1 } else { 0 };
for k in del_start..i {
if unified[k].origin == '-' {
deleted_lines.push(&unified[k]);
}
}
// If we have paired deletions, emit them as Modified pairs.
let pairs = deleted_lines.len().min(added_lines.len());
for k in 0..pairs {
let del = deleted_lines[k];
let add = added_lines[k];
result.push(SideBySideLine {
left_line_no: del.old_lineno,
right_line_no: add.new_lineno,
left_content: del.content.clone(),
right_content: add.content.clone(),
change_type: SideBySideChangeType::Modified,
});
}
// Remaining unpaired additions.
for k in pairs..added_lines.len() {
let add = added_lines[k];
result.push(SideBySideLine {
left_line_no: None,
right_line_no: add.new_lineno,
left_content: String::new(),
right_content: add.content.clone(),
change_type: SideBySideChangeType::Added,
});
}
// Remaining unpaired deletions (only possible if deletions > additions).
for k in pairs..deleted_lines.len() {
let del = deleted_lines[k];
result.push(SideBySideLine {
left_line_no: del.old_lineno,
right_line_no: None,
left_content: del.content.clone(),
right_content: String::new(),
change_type: SideBySideChangeType::Removed,
});
}
}
'-' => {
// Collect a run of consecutive deletions.
let mut deleted_lines: Vec<&DiffLine> = vec![line];
while i + 1 < unified.len() && unified[i + 1].origin == '-' {
i += 1;
deleted_lines.push(&unified[i]);
}
// Emit each deletion (unless already paired above).
// We defer pairing to the addition-handling block above,
// so here we just emit unpaired deletions.
for del in &deleted_lines {
result.push(SideBySideLine {
left_line_no: del.old_lineno,
right_line_no: None,
left_content: del.content.clone(),
right_content: String::new(),
change_type: SideBySideChangeType::Removed,
});
}
}
_ => {
// Context line — appears on both sides.
result.push(SideBySideLine {
left_line_no: line.old_lineno,
right_line_no: line.new_lineno,
left_content: line.content.clone(),
right_content: line.content.clone(),
change_type: SideBySideChangeType::Unchanged,
});
}
}
i += 1;
}
result
}