- 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
361 lines
11 KiB
Rust
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 })
|
|
}
|
|
}
|