This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / gitmirror / src / diff.rs
12 kB 365 lines
1use gix::ObjectId; 2use gix::bstr::ByteSlice as _; 3use gix::objs::tree::EntryKind; 4use line_numbers::{LineNumber, SingleLineSpan}; 5use rustc_hash::FxHashSet; 6use tracing::info; 7 8/// Blobs larger than this are treated as binary. 9const LARGE_FILE_THRESHOLD_BYTES: u64 = 1024 * 1024; 10 11#[derive(Debug)] 12pub struct FileContent { 13 pub(crate) path: String, 14 pub(crate) oid: String, 15 pub(crate) size: usize, 16 pub(crate) is_binary: bool, 17 pub(crate) is_submodule: bool, 18 /// Raw file bytes, inlined when the `oid` is not fetchable by the client (interdiff's 19 /// synthetic rebased tree). `None` for ordinary diffs and for binary/oversized blobs. 20 pub(crate) content: Option<Vec<u8>>, 21} 22 23impl From<FileContent> for crate::protocol::v1::FileContent { 24 fn from(f: FileContent) -> Self { 25 Self { 26 path: f.path, 27 oid: f.oid, 28 size: f.size as u64, 29 is_binary: f.is_binary, 30 is_submodule: f.is_submodule, 31 content: f.content, 32 } 33 } 34} 35 36#[derive(Debug, Clone)] 37pub struct Hunk { 38 pub(crate) novel_lhs: FxHashSet<LineNumber>, 39 pub(crate) novel_rhs: FxHashSet<LineNumber>, 40 pub(crate) lines: Vec<(Option<LineNumber>, Option<LineNumber>)>, 41} 42 43impl From<Hunk> for crate::protocol::v1::Hunk { 44 fn from(h: Hunk) -> Self { 45 // Sort the novel line sets for deterministic output. 46 let sorted = |set: FxHashSet<LineNumber>| -> Vec<u32> { 47 let mut v: Vec<u32> = set.into_iter().map(|n| n.0).collect(); 48 v.sort_unstable(); 49 v 50 }; 51 Self { 52 novel_lhs: sorted(h.novel_lhs), 53 novel_rhs: sorted(h.novel_rhs), 54 lines: h 55 .lines 56 .into_iter() 57 .map(|(lhs, rhs)| crate::protocol::v1::LinePair { 58 lhs: lhs.map(|n| n.0), 59 rhs: rhs.map(|n| n.0), 60 }) 61 .collect(), 62 } 63 } 64} 65 66/// A matched token (an atom, a delimiter, or a comment word). 67#[derive(PartialEq, Eq, Debug, Clone)] 68pub enum MatchKind { 69 // TBD 70} 71 72#[derive(Debug, Clone, PartialEq, Eq)] 73pub struct MatchedPos { 74 pub(crate) kind: MatchKind, 75 pub(crate) pos: SingleLineSpan, 76} 77 78#[derive(Debug)] 79pub struct Diff { 80 pub(crate) lhs_src: FileContent, 81 pub(crate) rhs_src: FileContent, 82 pub(crate) hunks: Vec<Hunk>, 83 84 #[allow(unused)] 85 pub(crate) lhs_positions: Vec<MatchedPos>, 86 #[allow(unused)] 87 pub(crate) rhs_positions: Vec<MatchedPos>, 88 89 /// If the two files do not have exactly the same bytes, the 90 /// number of bytes in each file. 91 pub(crate) has_byte_changes: Option<(usize, usize)>, 92 pub(crate) has_syntactic_changes: bool, 93} 94 95impl From<Diff> for crate::protocol::v1::FileDiff { 96 fn from(d: Diff) -> Self { 97 Self { 98 lhs_src: Some(d.lhs_src.into()), 99 rhs_src: Some(d.rhs_src.into()), 100 hunks: d.hunks.into_iter().map(Into::into).collect(), 101 has_byte_changes: d.has_byte_changes.map(|(lhs, rhs)| { 102 crate::protocol::v1::ByteChanges { 103 lhs: lhs as u64, 104 rhs: rhs as u64, 105 } 106 }), 107 has_syntactic_changes: d.has_syntactic_changes, 108 } 109 } 110} 111 112/// Diff two trees, yielding one [`Diff`] per changed file (line-level [`Hunk`]s included). 113/// 114/// Returns a lazy iterator of `anyhow::Result<Diff>`. 115pub fn diff<'repo>( 116 repo: &'repo gix::Repository, 117 old_tree: &gix::Tree<'_>, 118 new_tree: &gix::Tree<'_>, 119 embed_content: bool, 120) -> anyhow::Result<DiffIter<'repo>> { 121 let changes = repo.diff_tree_to_tree(Some(old_tree), Some(new_tree), None)?; 122 let mut cache = repo.diff_resource_cache_for_tree_diff()?; 123 cache.filter.options.large_file_threshold_bytes = LARGE_FILE_THRESHOLD_BYTES; 124 Ok(DiffIter { 125 repo, 126 changes: changes.into_iter(), 127 cache, 128 embed_content, 129 }) 130} 131 132/// Lazy iterator over per-file [`Diff`]s. 133pub struct DiffIter<'repo> { 134 repo: &'repo gix::Repository, 135 changes: std::vec::IntoIter<gix::object::tree::diff::ChangeDetached>, 136 cache: gix::diff::blob::Platform, 137 embed_content: bool, 138} 139 140impl Iterator for DiffIter<'_> { 141 type Item = anyhow::Result<Diff>; 142 143 fn next(&mut self) -> Option<Self::Item> { 144 loop { 145 let change = self.changes.next()?; 146 let (old, new) = change_sides(&change); 147 // Trees are recursed into by gix and never diffed directly; skip defensively. 148 if old.is_kind(EntryKind::Tree) || new.is_kind(EntryKind::Tree) { 149 continue; 150 } 151 // Submodules (gitlinks) can't go through the blob pipeline — their commit lives in 152 // another repo. Emit the commit oids directly for the caller to compare. 153 if old.is_kind(EntryKind::Commit) || new.is_kind(EntryKind::Commit) { 154 return Some(Ok(submodule_diff(self.repo, &change, old, new))); 155 } 156 let result = build_diff(self.repo, &change, &mut self.cache, self.embed_content); 157 self.cache.clear_resource_cache_keep_allocation(); 158 return Some(result); 159 } 160 } 161} 162 163enum Side { 164 Absent, 165 Present { oid: ObjectId, kind: EntryKind }, 166} 167 168impl Side { 169 fn is_kind(&self, wanted: EntryKind) -> bool { 170 matches!(self, Side::Present { kind, .. } if *kind == wanted) 171 } 172} 173 174/// Decompose a change into its `(old, new)` sides regardless of variant. 175fn change_sides(change: &gix::object::tree::diff::ChangeDetached) -> (Side, Side) { 176 use gix::object::tree::diff::ChangeDetached as C; 177 let present = |oid, mode: gix::object::tree::EntryMode| Side::Present { 178 oid, 179 kind: mode.kind(), 180 }; 181 match change { 182 C::Addition { id, entry_mode, .. } => (Side::Absent, present(*id, *entry_mode)), 183 C::Deletion { id, entry_mode, .. } => (present(*id, *entry_mode), Side::Absent), 184 C::Modification { 185 previous_id, 186 previous_entry_mode, 187 id, 188 entry_mode, 189 .. 190 } => ( 191 present(*previous_id, *previous_entry_mode), 192 present(*id, *entry_mode), 193 ), 194 C::Rewrite { 195 source_id, 196 source_entry_mode, 197 id, 198 entry_mode, 199 .. 200 } => ( 201 present(*source_id, *source_entry_mode), 202 present(*id, *entry_mode), 203 ), 204 } 205} 206 207/// Build a [`Diff`] for a submodule (gitlink) change: no hunks, both sides carry their commit 208/// oid with `is_submodule` set. The caller compares the oids as submodule pointers. 209fn submodule_diff( 210 repo: &gix::Repository, 211 change: &gix::object::tree::diff::ChangeDetached, 212 old: Side, 213 new: Side, 214) -> Diff { 215 let path = change.location().to_string(); 216 let side = |s: Side| FileContent { 217 path: path.clone(), 218 oid: match s { 219 Side::Absent => ObjectId::null(repo.object_hash()).to_string(), 220 Side::Present { oid, .. } => oid.to_string(), 221 }, 222 size: 0, 223 is_binary: false, 224 is_submodule: true, 225 content: None, 226 }; 227 Diff { 228 lhs_src: side(old), 229 rhs_src: side(new), 230 hunks: Vec::new(), 231 lhs_positions: Vec::new(), 232 rhs_positions: Vec::new(), 233 has_byte_changes: None, 234 has_syntactic_changes: false, 235 } 236} 237 238/// Build the [`Diff`] for a single changed file. 239fn build_diff( 240 repo: &gix::Repository, 241 change: &gix::object::tree::diff::ChangeDetached, 242 cache: &mut gix::diff::blob::Platform, 243 embed_content: bool, 244) -> anyhow::Result<Diff> { 245 use gix::diff::blob::platform::prepare_diff::Operation; 246 use gix::diff::blob::platform::resource::Data; 247 use gix::diff::blob::{Diff as BlobDiff, InternedInput}; 248 use gix::prelude::TreeDiffChangeExt as _; 249 250 let change = change.attach(repo, repo); 251 let blob = change.diff(cache)?; 252 let out = blob.resource_cache.prepare_diff()?; 253 254 // Per-side file metadata, derived from the prepared resources. 255 let side = 256 |res: &gix::diff::blob::platform::Resource<'_>, embed_content: bool| -> FileContent { 257 let (size, is_binary, content) = match res.data { 258 // Only textual buffers carry inlinable bytes; binary/missing sides fall back to oid. 259 Data::Buffer { buf, .. } => (buf.len(), false, embed_content.then(|| buf.to_vec())), 260 Data::Binary { size } => (size as usize, true, None), 261 Data::Missing => (0, false, None), 262 }; 263 FileContent { 264 path: res.rela_path.to_string(), 265 oid: res.id.to_string(), 266 size, 267 is_binary, 268 is_submodule: false, 269 content, 270 } 271 }; 272 let mut lhs_src = side(&out.old, embed_content); 273 let mut rhs_src = side(&out.new, false); 274 275 let has_byte_changes = if lhs_src.size == rhs_src.size && out.old.id == out.new.id { 276 None 277 } else { 278 Some((lhs_src.size, rhs_src.size)) 279 }; 280 281 let hunks = match out.operation { 282 Operation::InternalDiff { algorithm } => { 283 let input = InternedInput::new(out.old.intern_source(), out.new.intern_source()); 284 let mut d = BlobDiff::compute(algorithm, &input); 285 d.postprocess_lines(&input); 286 d.hunks() 287 .map(|h| { 288 let before: Vec<LineNumber> = h.before.clone().map(LineNumber).collect(); 289 let after: Vec<LineNumber> = h.after.clone().map(LineNumber).collect(); 290 let n = before.len().max(after.len()); 291 let lines = (0..n) 292 .map(|i| (before.get(i).copied(), after.get(i).copied())) 293 .collect(); 294 Hunk { 295 novel_lhs: before.iter().copied().collect(), 296 novel_rhs: after.iter().copied().collect(), 297 lines, 298 } 299 }) 300 .collect() 301 } 302 Operation::SourceOrDestinationIsBinary => { 303 lhs_src.is_binary = true; 304 rhs_src.is_binary = true; 305 Vec::new() 306 } 307 Operation::ExternalCommand { .. } => unreachable!("we disabled that"), 308 }; 309 310 Ok(Diff { 311 lhs_src, 312 rhs_src, 313 hunks, 314 lhs_positions: Vec::new(), 315 rhs_positions: Vec::new(), 316 has_byte_changes, 317 has_syntactic_changes: false, 318 }) 319} 320 321/// Compute the two trees whose diff is the interdiff between an old patch 322/// version (`from_base..from_head`) and a new one (`to_base..to_head`). 323/// 324/// Rebase `from_base..from_head` onto `to_base` and return rebased tree so 325/// caller can diff between `rebased_tree` and `to_head.tree` 326pub fn prepare_interdiff<'repo>( 327 repo: &'repo gix::Repository, 328 (from_base_id, from_head_id): (ObjectId, ObjectId), 329 to_base_id: ObjectId, 330) -> anyhow::Result<gix::Tree<'repo>> { 331 let from_head = repo.find_commit(from_head_id)?; 332 333 let from_base = repo.find_commit(from_base_id)?; 334 let to_base = repo.find_commit(to_base_id)?; 335 336 // Endpoint trees for the 3-way merge. 337 let from_base_tree = from_base.tree_id()?.detach(); 338 let from_head_tree = from_head.tree_id()?.detach(); 339 let to_base_tree = to_base.tree_id()?.detach(); 340 341 let options = repo.tree_merge_options()?; 342 let labels = gix::merge::blob::builtin_driver::text::Labels { 343 ancestor: Some("from.base".as_bytes().as_bstr()), 344 current: Some("to.base".as_bytes().as_bstr()), 345 other: Some("from.head".as_bytes().as_bstr()), 346 }; 347 let mut outcome = repo.merge_trees( 348 from_base_tree, 349 to_base_tree, 350 from_head_tree, 351 labels, 352 options, 353 )?; 354 355 if !outcome.conflicts.is_empty() { 356 info!( 357 conflicts = outcome.conflicts.len(), 358 "interdiff rebase produced conflicts; tree contains conflict markers" 359 ); 360 } 361 362 let rebased_id = outcome.tree.write()?.detach(); 363 let rebased_tree = repo.find_tree(rebased_id)?; 364 Ok(rebased_tree) 365}