gitdataai/lib/git/cmd/diff/diff_tree_to_tree.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

361 lines
11 KiB
Rust

use gix::{
bstr::ByteSlice,
diff::blob::unified_diff::{
ConsumeHunk, ContextSize, DiffLineKind, HunkHeader,
},
};
use crate::{
bare::GitBare,
cmd::{
diff::{
DiffDelta, DiffDeltaStatus, DiffFile, DiffHunk, DiffLine,
DiffOptions, DiffResult, DiffStats,
},
oid::ObjectId,
},
errors::{GitError, GitResult},
};
pub fn peel_to_tree(
repo: &gix::Repository,
oid: ObjectId,
) -> GitResult<gix::Tree<'_>> {
let gix_id: gix::hash::ObjectId = (&oid).try_into()?;
if let Ok(tree) = repo.find_tree(gix_id) {
Ok(tree)
} else {
let commit = repo
.find_commit(gix_id)
.map_err(|e| GitError::Gix(e.to_string()))?;
let tree_id = commit
.tree_id()
.map_err(|e| GitError::Gix(e.to_string()))?
.detach();
repo.find_tree(tree_id)
.map_err(|e| GitError::Gix(e.to_string()))
}
}
pub fn matches_pathspec(pathspec: &[String], path: &str) -> bool {
if pathspec.is_empty() {
return true;
}
pathspec
.iter()
.any(|spec| path == spec || path.starts_with(spec))
}
pub fn change_to_delta(
change: &gix::diff::tree_with_rewrites::Change,
) -> DiffDelta {
use gix::diff::tree_with_rewrites::Change;
match change {
Change::Addition { location, id, .. } => {
let path = location.to_str().unwrap_or("").to_string();
DiffDelta {
status: DiffDeltaStatus::Added,
old_file: DiffFile {
oid: None,
path: Some(path.clone()),
size: 0,
is_binary: false,
},
new_file: DiffFile {
oid: oid_to_option(id),
path: Some(path),
size: 0,
is_binary: false,
},
nfiles: 1,
hunks: Vec::new(),
lines: Vec::new(),
}
}
Change::Deletion { location, id, .. } => {
let path = location.to_str().unwrap_or("").to_string();
DiffDelta {
status: DiffDeltaStatus::Deleted,
old_file: DiffFile {
oid: oid_to_option(id),
path: Some(path.clone()),
size: 0,
is_binary: false,
},
new_file: DiffFile {
oid: None,
path: Some(path),
size: 0,
is_binary: false,
},
nfiles: 1,
hunks: Vec::new(),
lines: Vec::new(),
}
}
Change::Modification {
location,
previous_entry_mode,
previous_id,
entry_mode,
id,
} => {
let path = location.to_str().unwrap_or("").to_string();
let status = if previous_entry_mode.kind() != entry_mode.kind() {
DiffDeltaStatus::Typechange
} else {
DiffDeltaStatus::Modified
};
DiffDelta {
status,
old_file: DiffFile {
oid: oid_to_option(previous_id),
path: Some(path.clone()),
size: 0,
is_binary: false,
},
new_file: DiffFile {
oid: oid_to_option(id),
path: Some(path),
size: 0,
is_binary: false,
},
nfiles: 1,
hunks: Vec::new(),
lines: Vec::new(),
}
}
Change::Rewrite {
source_location,
source_id,
location,
id,
copy,
..
} => {
let old_path = source_location.to_str().unwrap_or("").to_string();
let new_path = location.to_str().unwrap_or("").to_string();
DiffDelta {
status: if *copy {
DiffDeltaStatus::Copied
} else {
DiffDeltaStatus::Renamed
},
old_file: DiffFile {
oid: oid_to_option(source_id),
path: Some(old_path),
size: 0,
is_binary: false,
},
new_file: DiffFile {
oid: oid_to_option(id),
path: Some(new_path),
size: 0,
is_binary: false,
},
nfiles: 2,
hunks: Vec::new(),
lines: Vec::new(),
}
}
}
}
pub fn oid_to_option(id: &gix::hash::oid) -> Option<ObjectId> {
if id.is_null() {
None
} else {
Some(ObjectId::new(id.to_hex().to_string()))
}
}
pub struct HunkCollector {
hunks: Vec<DiffHunk>,
lines: Vec<DiffLine>,
current_old_lineno: u32,
current_new_lineno: u32,
}
impl HunkCollector {
pub fn new() -> Self {
HunkCollector {
hunks: Vec::new(),
lines: Vec::new(),
current_old_lineno: 0,
current_new_lineno: 0,
}
}
}
impl ConsumeHunk for HunkCollector {
type Out = (Vec<DiffHunk>, Vec<DiffLine>);
fn consume_hunk(
&mut self,
header: HunkHeader,
entries: &[(DiffLineKind, &[u8])],
) -> std::io::Result<()> {
self.current_old_lineno = header.before_hunk_start;
self.current_new_lineno = header.after_hunk_start;
self.hunks.push(DiffHunk {
old_start: header.before_hunk_start,
old_lines: header.before_hunk_len,
new_start: header.after_hunk_start,
new_lines: header.after_hunk_len,
header: format!(
"@@ -{},{} +{},{} @@",
header.before_hunk_start,
header.before_hunk_len,
header.after_hunk_start,
header.after_hunk_len
),
});
for (kind, content) in entries {
let origin = kind.to_prefix();
let content_str = String::from_utf8_lossy(content).to_string();
let (old_lineno, new_lineno) = match kind {
DiffLineKind::Context => {
let old = Some(self.current_old_lineno);
let new = Some(self.current_new_lineno);
self.current_old_lineno += 1;
self.current_new_lineno += 1;
(old, new)
}
DiffLineKind::Add => {
let new = Some(self.current_new_lineno);
self.current_new_lineno += 1;
(None, new)
}
DiffLineKind::Remove => {
let old = self.current_old_lineno;
self.current_old_lineno += 1;
(Some(old), None)
}
};
self.lines.push(DiffLine {
content: content_str,
origin,
old_lineno,
new_lineno,
num_lines: 1,
content_offset: -1,
});
}
Ok(())
}
fn finish(self) -> (Vec<DiffHunk>, Vec<DiffLine>) {
(self.hunks, self.lines)
}
}
impl GitBare {
pub fn diff_tree_to_tree(
&self,
old_tree: ObjectId,
new_tree: ObjectId,
opts: Option<DiffOptions>,
) -> GitResult<DiffResult> {
let repo = self.gix_repo()?;
let options = opts.unwrap_or_default();
let old_tree_obj = peel_to_tree(&repo, old_tree)?;
let new_tree_obj = peel_to_tree(&repo, new_tree)?;
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);
// Skip directories — only diff blobs
if change.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;
if options.context_lines > 0 {
let ctx = ContextSize::symmetrical(
options.context_lines.max(3),
);
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 })
}
}