- 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
365 lines
12 KiB
Rust
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
|
|
}
|
|
}
|