Monorepo for Tangled
0

Configure Feed

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

core / gitmirror / src / diff.rs
13 kB 370 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 an iterator of `anyhow::Result<Diff>`: per-file diffing can fail independently, so 115/// each item is fallible. The consumer drives it lazily and can stop at any point by dropping 116/// the iterator (e.g. when the gRPC client hangs up). 117pub fn diff<'repo>( 118 repo: &'repo gix::Repository, 119 old_tree: &gix::Tree<'_>, 120 new_tree: &gix::Tree<'_>, 121 embed_content: bool, 122) -> anyhow::Result<DiffIter<'repo>> { 123 let changes = repo.diff_tree_to_tree(Some(old_tree), Some(new_tree), None)?; 124 let mut cache = repo.diff_resource_cache_for_tree_diff()?; 125 cache.filter.options.large_file_threshold_bytes = LARGE_FILE_THRESHOLD_BYTES; 126 Ok(DiffIter { 127 repo, 128 changes: changes.into_iter(), 129 cache, 130 embed_content, 131 }) 132} 133 134/// Lazy iterator over per-file [`Diff`]s. Created by [`diff`]. 135pub struct DiffIter<'repo> { 136 repo: &'repo gix::Repository, 137 changes: std::vec::IntoIter<gix::object::tree::diff::ChangeDetached>, 138 cache: gix::diff::blob::Platform, 139 embed_content: bool, 140} 141 142impl Iterator for DiffIter<'_> { 143 type Item = anyhow::Result<Diff>; 144 145 fn next(&mut self) -> Option<Self::Item> { 146 loop { 147 let change = self.changes.next()?; 148 let (old, new) = change_sides(&change); 149 // Trees are recursed into by gix and never diffed directly; skip defensively. 150 if old.is_kind(EntryKind::Tree) || new.is_kind(EntryKind::Tree) { 151 continue; 152 } 153 // Submodules (gitlinks) can't go through the blob pipeline — their commit lives in 154 // another repo. Emit the commit oids directly for the caller to compare. 155 if old.is_kind(EntryKind::Commit) || new.is_kind(EntryKind::Commit) { 156 return Some(Ok(submodule_diff(self.repo, &change, old, new))); 157 } 158 let result = build_diff(self.repo, &change, &mut self.cache, self.embed_content); 159 self.cache.clear_resource_cache_keep_allocation(); 160 return Some(result); 161 } 162 } 163} 164 165/// One side (old or new) of a change. 166enum Side { 167 Absent, 168 Present { oid: ObjectId, kind: EntryKind }, 169} 170 171impl Side { 172 fn is_kind(&self, wanted: EntryKind) -> bool { 173 matches!(self, Side::Present { kind, .. } if *kind == wanted) 174 } 175} 176 177/// Decompose a change into its `(old, new)` sides regardless of variant. 178fn change_sides(change: &gix::object::tree::diff::ChangeDetached) -> (Side, Side) { 179 use gix::object::tree::diff::ChangeDetached as C; 180 let present = |oid, mode: gix::object::tree::EntryMode| Side::Present { 181 oid, 182 kind: mode.kind(), 183 }; 184 match change { 185 C::Addition { id, entry_mode, .. } => (Side::Absent, present(*id, *entry_mode)), 186 C::Deletion { id, entry_mode, .. } => (present(*id, *entry_mode), Side::Absent), 187 C::Modification { 188 previous_id, 189 previous_entry_mode, 190 id, 191 entry_mode, 192 .. 193 } => ( 194 present(*previous_id, *previous_entry_mode), 195 present(*id, *entry_mode), 196 ), 197 C::Rewrite { 198 source_id, 199 source_entry_mode, 200 id, 201 entry_mode, 202 .. 203 } => ( 204 present(*source_id, *source_entry_mode), 205 present(*id, *entry_mode), 206 ), 207 } 208} 209 210/// Build a [`Diff`] for a submodule (gitlink) change: no hunks, both sides carry their commit 211/// oid with `is_submodule` set. The caller compares the oids as submodule pointers. 212fn submodule_diff( 213 repo: &gix::Repository, 214 change: &gix::object::tree::diff::ChangeDetached, 215 old: Side, 216 new: Side, 217) -> Diff { 218 let path = change.location().to_string(); 219 let side = |s: Side| FileContent { 220 path: path.clone(), 221 oid: match s { 222 Side::Absent => ObjectId::null(repo.object_hash()).to_string(), 223 Side::Present { oid, .. } => oid.to_string(), 224 }, 225 size: 0, 226 is_binary: false, 227 is_submodule: true, 228 content: None, 229 }; 230 Diff { 231 lhs_src: side(old), 232 rhs_src: side(new), 233 hunks: Vec::new(), 234 lhs_positions: Vec::new(), 235 rhs_positions: Vec::new(), 236 has_byte_changes: None, 237 has_syntactic_changes: false, 238 } 239} 240 241/// Build the [`Diff`] for a single changed file. 242fn build_diff( 243 repo: &gix::Repository, 244 change: &gix::object::tree::diff::ChangeDetached, 245 cache: &mut gix::diff::blob::Platform, 246 embed_content: bool, 247) -> anyhow::Result<Diff> { 248 use gix::diff::blob::platform::prepare_diff::Operation; 249 use gix::diff::blob::platform::resource::Data; 250 use gix::diff::blob::{Diff as BlobDiff, InternedInput}; 251 use gix::prelude::TreeDiffChangeExt as _; 252 253 let change = change.attach(repo, repo); 254 let blob = change.diff(cache)?; 255 let out = blob.resource_cache.prepare_diff()?; 256 257 // Per-side file metadata, derived from the prepared resources. 258 let side = 259 |res: &gix::diff::blob::platform::Resource<'_>, embed_content: bool| -> FileContent { 260 let (size, is_binary, content) = match res.data { 261 // Only textual buffers carry inlinable bytes; binary/missing sides fall back to oid. 262 Data::Buffer { buf, .. } => (buf.len(), false, embed_content.then(|| buf.to_vec())), 263 Data::Binary { size } => (size as usize, true, None), 264 Data::Missing => (0, false, None), 265 }; 266 FileContent { 267 path: res.rela_path.to_string(), 268 oid: res.id.to_string(), 269 size, 270 is_binary, 271 is_submodule: false, 272 content, 273 } 274 }; 275 let mut lhs_src = side(&out.old, embed_content); 276 let mut rhs_src = side(&out.new, false); 277 278 let has_byte_changes = if lhs_src.size == rhs_src.size && out.old.id == out.new.id { 279 None 280 } else { 281 Some((lhs_src.size, rhs_src.size)) 282 }; 283 284 let hunks = match out.operation { 285 Operation::InternalDiff { algorithm } => { 286 let input = InternedInput::new(out.old.intern_source(), out.new.intern_source()); 287 let mut d = BlobDiff::compute(algorithm, &input); 288 d.postprocess_lines(&input); 289 d.hunks() 290 .map(|h| { 291 let before: Vec<LineNumber> = h.before.clone().map(LineNumber).collect(); 292 let after: Vec<LineNumber> = h.after.clone().map(LineNumber).collect(); 293 let n = before.len().max(after.len()); 294 let lines = (0..n) 295 .map(|i| (before.get(i).copied(), after.get(i).copied())) 296 .collect(); 297 Hunk { 298 novel_lhs: before.iter().copied().collect(), 299 novel_rhs: after.iter().copied().collect(), 300 lines, 301 } 302 }) 303 .collect() 304 } 305 Operation::SourceOrDestinationIsBinary => { 306 lhs_src.is_binary = true; 307 rhs_src.is_binary = true; 308 Vec::new() 309 } 310 Operation::ExternalCommand { .. } => unreachable!("we disabled that"), 311 }; 312 313 Ok(Diff { 314 lhs_src, 315 rhs_src, 316 hunks, 317 lhs_positions: Vec::new(), 318 rhs_positions: Vec::new(), 319 has_byte_changes, 320 has_syntactic_changes: false, 321 }) 322} 323 324/// Compute the two trees whose diff is the interdiff between an old patch 325/// version (`from_base..from_head`) and a new one (`to_base..to_head`). 326/// 327/// Rebase `from_base..from_head` onto `to_base` and return rebased tree so 328/// caller can diff between `rebased_tree` and `to_head.tree` 329pub fn prepare_interdiff<'repo>( 330 repo: &'repo gix::Repository, 331 (from_base_id, from_head_id): (ObjectId, ObjectId), 332 to_base_id: ObjectId, 333) -> anyhow::Result<gix::Tree<'repo>> { 334 let from_head = repo.find_commit(from_head_id)?; 335 336 let from_base = repo.find_commit(from_base_id)?; 337 let to_base = repo.find_commit(to_base_id)?; 338 339 // Endpoint trees for the 3-way merge. 340 let from_base_tree = from_base.tree_id()?.detach(); 341 let from_head_tree = from_head.tree_id()?.detach(); 342 let to_base_tree = to_base.tree_id()?.detach(); 343 344 // Rebase = replay (from_base_tree -> from_head_tree) onto to_base_tree: 345 // ancestor = from_base_tree, ours = to_base_tree, theirs = from_head_tree. 346 let options = repo.tree_merge_options()?; 347 let labels = gix::merge::blob::builtin_driver::text::Labels { 348 ancestor: Some("from.base".as_bytes().as_bstr()), 349 current: Some("to.base".as_bytes().as_bstr()), 350 other: Some("from.head".as_bytes().as_bstr()), 351 }; 352 let mut outcome = repo.merge_trees( 353 from_base_tree, 354 to_base_tree, 355 from_head_tree, 356 labels, 357 options, 358 )?; 359 360 if !outcome.conflicts.is_empty() { 361 info!( 362 conflicts = outcome.conflicts.len(), 363 "interdiff rebase produced conflicts; tree contains conflict markers" 364 ); 365 } 366 367 let rebased_id = outcome.tree.write()?.detach(); 368 let rebased_tree = repo.find_tree(rebased_id)?; 369 Ok(rebased_tree) 370}