gitdataai/lib/git/cmd/diff/diff_patch.rs
zhenyi 779e4eae2f feat(channel): add article feed and composer with room type support
- Add ArticleFeed component for article-based channels
- Implement ArticleComposer with draft persistence
- Add Newspaper icon for article room type
- Update ChannelPage to conditionally render article feed vs message view
- Add article-related API endpoints and models
- Reset thread view when switching rooms
- Add room type check in channel sidebar
- Update CSS to hide scrollbars globally
- Add gRPC message size limit configuration
- Fix git diff tree handling
2026-05-31 03:09:49 +08:00

365 lines
12 KiB
Rust

use gix::{
bstr::ByteSlice,
diff::blob::unified_diff::{
ConsumeHunk, ContextSize, DiffLineKind, HunkHeader,
},
};
use crate::{
bare::GitBare,
cmd::{
diff::{
DiffDeltaStatus, DiffOptions, DiffResult, DiffStats,
SideBySideChangeType, SideBySideDiffResult, SideBySideFile,
SideBySideLine,
diff_tree_to_tree::{
HunkCollector, change_to_delta, matches_pathspec, peel_to_tree,
},
},
oid::ObjectId,
},
errors::{GitError, GitResult},
};
impl GitBare {
pub fn diff_patch(
&self,
old_commit: ObjectId,
new_commit: ObjectId,
opts: Option<DiffOptions>,
) -> GitResult<DiffResult> {
let repo = self.gix_repo()?;
let options = opts.unwrap_or_default();
let context_lines = options.context_lines.max(3);
let old_tree_obj = peel_to_tree(&repo, old_commit)?;
let new_tree_obj = peel_to_tree(&repo, new_commit)?;
let mut diff_opts = gix::diff::Options::default();
diff_opts.track_path();
let changes = repo
.diff_tree_to_tree(&old_tree_obj, &new_tree_obj, Some(diff_opts))
.map_err(|e| GitError::Gix(e.to_string()))?;
let mut resource_cache = repo
.diff_resource_cache_for_tree_diff()
.map_err(|e| GitError::Gix(e.to_string()))?;
let mut deltas = Vec::new();
let mut stats = DiffStats {
files_changed: 0,
insertions: 0,
deletions: 0,
};
for change in &changes {
let location = change.location().to_str().unwrap_or("");
if !matches_pathspec(&options.pathspec, location) {
continue;
}
let mut delta = change_to_delta(change);
// Only diff blobs — skip trees (directories)
let entry_mode = change.entry_mode();
if entry_mode.is_tree() {
resource_cache.clear_resource_cache_keep_allocation();
continue;
}
resource_cache
.set_resource_by_change(change.to_ref(), &repo.objects)
.map_err(|e| GitError::Gix(e.to_string()))?;
let is_binary = {
use gix::diff::blob::platform::prepare_diff::Operation;
let prep = resource_cache
.prepare_diff()
.map_err(|e| GitError::Gix(e.to_string()))?;
match prep.operation {
Operation::InternalDiff { algorithm } => {
let input = prep.interned_input();
let diff = gix::diff::blob::diff_with_slider_heuristics(
algorithm, &input,
);
stats.files_changed += 1;
stats.insertions += diff.count_additions() as usize;
stats.deletions += diff.count_removals() as usize;
let ctx = ContextSize::symmetrical(context_lines);
let collector = HunkCollector::new();
let unified = gix::diff::blob::UnifiedDiff::new(
&diff, &input, collector, ctx,
);
let (hunks, lines) = unified
.consume()
.map_err(|e| GitError::Gix(e.to_string()))?;
delta.hunks = hunks;
delta.lines = lines;
false
}
Operation::SourceOrDestinationIsBinary => {
stats.files_changed += 1;
true
}
Operation::ExternalCommand { .. } => {
stats.files_changed += 1;
false
}
}
};
if is_binary {
delta.old_file.is_binary = true;
delta.new_file.is_binary = true;
}
resource_cache.clear_resource_cache_keep_allocation();
deltas.push(delta);
}
Ok(DiffResult { stats, deltas })
}
pub fn diff_patch_side_by_side(
&self,
old_commit: ObjectId,
new_commit: ObjectId,
opts: Option<DiffOptions>,
) -> GitResult<SideBySideDiffResult> {
let repo = self.gix_repo()?;
let options = opts.unwrap_or_default();
let context_lines = options.context_lines.max(3);
let old_tree_obj = peel_to_tree(&repo, old_commit)?;
let new_tree_obj = peel_to_tree(&repo, new_commit)?;
let mut diff_opts = gix::diff::Options::default();
diff_opts.track_path();
let changes = repo
.diff_tree_to_tree(&old_tree_obj, &new_tree_obj, Some(diff_opts))
.map_err(|e| GitError::Gix(e.to_string()))?;
let mut resource_cache = repo
.diff_resource_cache_for_tree_diff()
.map_err(|e| GitError::Gix(e.to_string()))?;
let mut files: Vec<SideBySideFile> = Vec::new();
let mut total_additions = 0;
let mut total_deletions = 0;
for change in &changes {
let location = change.location().to_str().unwrap_or("");
if !matches_pathspec(&options.pathspec, location) {
continue;
}
let delta = change_to_delta(change);
let is_rename = delta.status == DiffDeltaStatus::Renamed;
let path = delta.new_file.path.clone().unwrap_or_default();
// Skip directories — only diff blobs
if change.entry_mode().is_tree() {
total_additions += 0;
total_deletions += 0;
files.push(SideBySideFile {
path,
additions: 0,
deletions: 0,
is_binary: false,
is_rename,
lines: Vec::new(),
});
resource_cache.clear_resource_cache_keep_allocation();
continue;
}
resource_cache
.set_resource_by_change(change.to_ref(), &repo.objects)
.map_err(|e| GitError::Gix(e.to_string()))?;
let (file_additions, file_deletions, is_binary, sbs_lines) = {
use gix::diff::blob::platform::prepare_diff::Operation;
let prep = resource_cache
.prepare_diff()
.map_err(|e| GitError::Gix(e.to_string()))?;
match prep.operation {
Operation::InternalDiff { algorithm } => {
let input = prep.interned_input();
let diff = gix::diff::blob::diff_with_slider_heuristics(
algorithm, &input,
);
let adds = diff.count_additions() as usize;
let dels = diff.count_removals() as usize;
let ctx = ContextSize::symmetrical(context_lines);
let collector = SideBySideCollector::new();
let unified = gix::diff::blob::UnifiedDiff::new(
&diff, &input, collector, ctx,
);
let lines = unified
.consume()
.map_err(|e| GitError::Gix(e.to_string()))?;
(adds, dels, false, lines)
}
Operation::SourceOrDestinationIsBinary => {
(0, 0, true, Vec::new())
}
Operation::ExternalCommand { .. } => {
(0, 0, false, Vec::new())
}
}
};
total_additions += file_additions;
total_deletions += file_deletions;
files.push(SideBySideFile {
path,
additions: file_additions,
deletions: file_deletions,
is_binary,
is_rename,
lines: sbs_lines,
});
resource_cache.clear_resource_cache_keep_allocation();
}
Ok(SideBySideDiffResult {
files,
total_additions,
total_deletions,
})
}
}
struct SideBySideCollector {
lines: Vec<SideBySideLine>,
pending_removed: Vec<(u32, String)>,
pending_added: Vec<(u32, String)>,
current_old_lineno: u32,
current_new_lineno: u32,
}
impl SideBySideCollector {
fn new() -> Self {
SideBySideCollector {
lines: Vec::new(),
pending_removed: Vec::new(),
pending_added: Vec::new(),
current_old_lineno: 0,
current_new_lineno: 0,
}
}
fn flush_pending(&mut self) {
let removed_count = self.pending_removed.len();
let added_count = self.pending_added.len();
let common = removed_count.min(added_count);
for i in 0..common {
let (left_no, old_content) = &self.pending_removed[i];
let (right_no, new_content) = &self.pending_added[i];
let change_type = if old_content == new_content {
SideBySideChangeType::Unchanged
} else {
SideBySideChangeType::Modified
};
self.lines.push(SideBySideLine {
left_line_no: Some(*left_no),
right_line_no: Some(*right_no),
left_content: old_content.clone(),
right_content: new_content.clone(),
change_type,
});
}
for i in common..removed_count {
let (left_no, old_content) = &self.pending_removed[i];
self.lines.push(SideBySideLine {
left_line_no: Some(*left_no),
right_line_no: None,
left_content: old_content.clone(),
right_content: String::new(),
change_type: SideBySideChangeType::Removed,
});
}
for i in common..added_count {
let (right_no, new_content) = &self.pending_added[i];
self.lines.push(SideBySideLine {
left_line_no: None,
right_line_no: Some(*right_no),
left_content: String::new(),
right_content: new_content.clone(),
change_type: SideBySideChangeType::Added,
});
}
self.pending_removed.clear();
self.pending_added.clear();
}
}
impl ConsumeHunk for SideBySideCollector {
type Out = Vec<SideBySideLine>;
fn consume_hunk(
&mut self,
header: HunkHeader,
entries: &[(DiffLineKind, &[u8])],
) -> std::io::Result<()> {
self.flush_pending();
self.current_old_lineno = header.before_hunk_start;
self.current_new_lineno = header.after_hunk_start;
for (kind, content) in entries {
let content_str = String::from_utf8_lossy(content).to_string();
match kind {
DiffLineKind::Context => {
self.flush_pending();
self.lines.push(SideBySideLine {
left_line_no: Some(self.current_old_lineno),
right_line_no: Some(self.current_new_lineno),
left_content: content_str.clone(),
right_content: content_str,
change_type: SideBySideChangeType::Unchanged,
});
self.current_old_lineno += 1;
self.current_new_lineno += 1;
}
DiffLineKind::Add => {
self.pending_added
.push((self.current_new_lineno, content_str));
self.current_new_lineno += 1;
}
DiffLineKind::Remove => {
self.pending_removed
.push((self.current_old_lineno, content_str));
self.current_old_lineno += 1;
}
}
}
Ok(())
}
fn finish(mut self) -> Vec<SideBySideLine> {
self.flush_pending();
self.lines
}
}