This repository has no description
0

Configure Feed

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

core / knot2 / crates / knot-git / src / patch.rs
13 kB 390 lines
1use std::convert::Infallible; 2use std::ops::ControlFlow; 3 4use gix::diff::blob::unified_diff::{ConsumeHunk, ContextSize, DiffLineKind, HunkHeader}; 5use gix::diff::blob::{Algorithm, Diff, InternedInput, UnifiedDiff}; 6use knot_types::{ChangedFiles, ChangedFilesBudget, Listing, Oid, RepoPath}; 7 8use crate::error::{GitError, backend}; 9use crate::objects::EntryKind; 10use crate::repo::Repo; 11 12const BINARY_SNIFF_BYTES: usize = 8000; 13pub const MAX_DIFF_BLOB_BYTES: u64 = 25 * 1024 * 1024; 14 15#[derive(Debug, Clone, Copy, PartialEq, Eq)] 16pub enum LineOp { 17 Context, 18 Delete, 19 Add, 20} 21 22#[derive(Debug, Clone, PartialEq, Eq)] 23pub struct HunkLine { 24 pub op: LineOp, 25 pub text: Vec<u8>, 26} 27 28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 29pub struct PatchRange { 30 pub base: Option<Oid>, 31 pub head: Oid, 32} 33 34// Just making sure a count in a start slot doesn't even compile. 35knot_types::scalar_newtype! { 36 pub struct LineNumber(u32); 37 pub struct LineCount(u32); 38} 39 40#[derive(Debug, Clone, PartialEq, Eq)] 41pub struct Hunk { 42 pub old_start: LineNumber, 43 pub old_lines: LineCount, 44 pub new_start: LineNumber, 45 pub new_lines: LineCount, 46 pub lines: Vec<HunkLine>, 47} 48 49impl Hunk { 50 pub fn added(&self) -> LineCount { 51 self.count(LineOp::Add) 52 } 53 54 pub fn deleted(&self) -> LineCount { 55 self.count(LineOp::Delete) 56 } 57 58 fn count(&self, op: LineOp) -> LineCount { 59 LineCount::new( 60 self.lines 61 .iter() 62 .filter(|line| line.op == op) 63 .count() 64 .try_into() 65 .unwrap_or(u32::MAX), 66 ) 67 } 68} 69 70#[derive(Debug, Clone, Copy, PartialEq, Eq)] 71pub enum PatchStatus { 72 Added, 73 Deleted, 74 Modified, 75} 76 77#[derive(Debug, Clone, PartialEq, Eq)] 78pub struct FilePatch { 79 pub status: PatchStatus, 80 pub path: RepoPath, 81 pub old_oid: Oid, 82 pub new_oid: Oid, 83 pub old_kind: Option<EntryKind>, 84 pub new_kind: Option<EntryKind>, 85 pub is_binary: bool, 86 pub hunks: Vec<Hunk>, 87} 88 89fn is_binary(content: &[u8]) -> bool { 90 content[..content.len().min(BINARY_SNIFF_BYTES)].contains(&0) 91} 92 93struct CollectHunks { 94 hunks: Vec<Hunk>, 95} 96 97impl ConsumeHunk for CollectHunks { 98 type Out = Vec<Hunk>; 99 100 fn consume_hunk( 101 &mut self, 102 header: HunkHeader, 103 lines: &[(DiffLineKind, &[u8])], 104 ) -> std::io::Result<()> { 105 let map_op = |kind: DiffLineKind| match kind { 106 DiffLineKind::Context => LineOp::Context, 107 DiffLineKind::Remove => LineOp::Delete, 108 DiffLineKind::Add => LineOp::Add, 109 }; 110 let adjust = |start: u32, len: u32| { 111 if len == 0 { 112 start.saturating_sub(1) 113 } else { 114 start 115 } 116 }; 117 self.hunks.push(Hunk { 118 old_start: LineNumber::new(adjust(header.before_hunk_start, header.before_hunk_len)), 119 old_lines: LineCount::new(header.before_hunk_len), 120 new_start: LineNumber::new(adjust(header.after_hunk_start, header.after_hunk_len)), 121 new_lines: LineCount::new(header.after_hunk_len), 122 lines: lines 123 .iter() 124 .map(|(kind, text)| HunkLine { 125 op: map_op(*kind), 126 text: text.to_vec(), 127 }) 128 .collect(), 129 }); 130 Ok(()) 131 } 132 133 fn finish(self) -> Self::Out { 134 self.hunks 135 } 136} 137 138fn text_hunks(old: &[u8], new: &[u8]) -> Result<Vec<Hunk>, GitError> { 139 let input = InternedInput::new(old, new); 140 let diff = Diff::compute(Algorithm::Histogram, &input); 141 UnifiedDiff::new( 142 &diff, 143 &input, 144 CollectHunks { hunks: Vec::new() }, 145 ContextSize::symmetrical(3), 146 ) 147 .consume() 148 .map_err(backend) 149} 150 151enum Side { 152 Absent, 153 Present { oid: Oid, kind: EntryKind }, 154} 155 156impl Side { 157 fn oid(&self, absent: Oid) -> Oid { 158 match self { 159 Side::Absent => absent, 160 Side::Present { oid, .. } => *oid, 161 } 162 } 163 164 fn kind(&self) -> Option<EntryKind> { 165 match self { 166 Side::Absent => None, 167 Side::Present { kind, .. } => Some(*kind), 168 } 169 } 170} 171 172impl Repo { 173 fn patch_content(&self, side: &Side) -> Result<Vec<u8>, GitError> { 174 match side { 175 Side::Absent => Ok(Vec::new()), 176 Side::Present { oid, kind } => match kind { 177 EntryKind::Commit => { 178 Ok(format!("Subproject commit {}\n", oid.to_hex()).into_bytes()) 179 } 180 EntryKind::Tree => Ok(Vec::new()), 181 _ => self.read_blob(*oid), 182 }, 183 } 184 } 185 186 fn side_within_diff_budget(&self, side: &Side) -> Result<bool, GitError> { 187 match side { 188 Side::Present { 189 oid, 190 kind: EntryKind::Blob | EntryKind::BlobExecutable | EntryKind::Link, 191 } => Ok(self.blob_size(*oid)? <= MAX_DIFF_BLOB_BYTES), 192 _ => Ok(true), 193 } 194 } 195 196 fn file_patch( 197 &self, 198 status: PatchStatus, 199 path: RepoPath, 200 old: Side, 201 new: Side, 202 ) -> Result<FilePatch, GitError> { 203 let within_budget = 204 self.side_within_diff_budget(&old)? && self.side_within_diff_budget(&new)?; 205 let (binary, hunks) = match within_budget { 206 false => (true, Vec::new()), 207 true => { 208 let old_content = self.patch_content(&old)?; 209 let new_content = self.patch_content(&new)?; 210 let binary = is_binary(&old_content) || is_binary(&new_content); 211 let hunks = match binary { 212 true => Vec::new(), 213 false => text_hunks(&old_content, &new_content)?, 214 }; 215 (binary, hunks) 216 } 217 }; 218 Ok(FilePatch { 219 status, 220 path, 221 old_oid: old.oid(self.object_format().null_oid()), 222 new_oid: new.oid(self.object_format().null_oid()), 223 old_kind: old.kind(), 224 new_kind: new.kind(), 225 is_binary: binary, 226 hunks, 227 }) 228 } 229 230 fn diff_trees(&self, range: PatchRange) -> Result<(gix::Tree<'_>, gix::Tree<'_>), GitError> { 231 let PatchRange { 232 base: old_commit, 233 head: new_commit, 234 } = range; 235 let new_tree = self.root_tree(self.peel_to_commit(new_commit)?)?; 236 let old_tree = match old_commit { 237 Some(commit) => self.root_tree(self.peel_to_commit(commit)?)?, 238 None => self.git().empty_tree(), 239 }; 240 Ok((old_tree, new_tree)) 241 } 242 243 pub fn changed_paths(&self, range: PatchRange) -> Result<ChangedFiles, GitError> { 244 let (old_tree, new_tree) = self.diff_trees(range)?; 245 let mut budget = ChangedFilesBudget::new(); 246 let walked = old_tree 247 .changes() 248 .map_err(backend)? 249 .options(|options| { 250 options.track_rewrites(None); 251 }) 252 .for_each_to_obtain_tree(&new_tree, |change| -> Result<ControlFlow<()>, Infallible> { 253 use gix::object::tree::diff::Change; 254 let (location, is_tree) = match change { 255 Change::Addition { 256 location, 257 entry_mode, 258 .. 259 } 260 | Change::Deletion { 261 location, 262 entry_mode, 263 .. 264 } => (location, entry_mode.is_tree()), 265 Change::Modification { 266 location, 267 previous_entry_mode, 268 entry_mode, 269 .. 270 } => ( 271 location, 272 previous_entry_mode.is_tree() || entry_mode.is_tree(), 273 ), 274 Change::Rewrite { .. } => return Ok(ControlFlow::Continue(())), 275 }; 276 match (is_tree, RepoPath::new(location.to_string())) { 277 (true, _) => Ok(ControlFlow::Continue(())), 278 (false, Ok(path)) => Ok(budget.admit(path)), 279 (false, Err(_)) => Ok(budget.truncate()), 280 } 281 }); 282 let changed = budget.finish(); 283 // When the above gives us `Break`, 284 // gix doesn't return partial-success 285 // but instead `Error::Cancelled`. 286 // So if the listing comes out truncated, 287 // the "error" in `walked` is our own stop-sign given 288 // back at us and we ignore it on purpose. 289 // If the listing is complete, 290 // nothing ever asked to stop 291 // and when `walked` errors out it's actually 292 // from the diff itself that we should believe. 293 match changed.listing() { 294 Listing::Truncated => Ok(changed), 295 Listing::Complete => walked.map(|_| changed).map_err(backend), 296 } 297 } 298 299 pub fn commit_patches(&self, range: PatchRange) -> Result<Vec<FilePatch>, GitError> { 300 let (old_tree, new_tree) = self.diff_trees(range)?; 301 let mut sides: Vec<(PatchStatus, String, Side, Side)> = Vec::new(); 302 old_tree 303 .changes() 304 .map_err(backend)? 305 .options(|options| { 306 options.track_rewrites(None); 307 }) 308 .for_each_to_obtain_tree(&new_tree, |change| { 309 use gix::object::tree::diff::Change; 310 match change { 311 Change::Addition { 312 location, 313 id, 314 entry_mode, 315 .. 316 } => sides.push(( 317 PatchStatus::Added, 318 location.to_string(), 319 Side::Absent, 320 Side::Present { 321 oid: Oid::from(id.detach()), 322 kind: crate::objects::map_kind(entry_mode.kind()), 323 }, 324 )), 325 Change::Deletion { 326 location, 327 id, 328 entry_mode, 329 .. 330 } => sides.push(( 331 PatchStatus::Deleted, 332 location.to_string(), 333 Side::Present { 334 oid: Oid::from(id.detach()), 335 kind: crate::objects::map_kind(entry_mode.kind()), 336 }, 337 Side::Absent, 338 )), 339 Change::Modification { 340 location, 341 previous_id, 342 id, 343 previous_entry_mode, 344 entry_mode, 345 } => sides.push(( 346 PatchStatus::Modified, 347 location.to_string(), 348 Side::Present { 349 oid: Oid::from(previous_id.detach()), 350 kind: crate::objects::map_kind(previous_entry_mode.kind()), 351 }, 352 Side::Present { 353 oid: Oid::from(id.detach()), 354 kind: crate::objects::map_kind(entry_mode.kind()), 355 }, 356 )), 357 Change::Rewrite { .. } => {} 358 } 359 Ok::<_, std::convert::Infallible>(std::ops::ControlFlow::Continue(())) 360 }) 361 .map_err(backend)?; 362 363 sides 364 .into_iter() 365 .filter(|(_, _, old, new)| { 366 !matches!( 367 (old, new), 368 ( 369 Side::Present { 370 kind: EntryKind::Tree, 371 .. 372 }, 373 _ 374 ) | ( 375 _, 376 Side::Present { 377 kind: EntryKind::Tree, 378 .. 379 } 380 ) 381 ) 382 }) 383 .map(|(status, path, old, new)| { 384 let path = 385 RepoPath::new(path).map_err(|error| GitError::Decode(error.to_string()))?; 386 self.file_patch(status, path, old, new) 387 }) 388 .collect() 389 } 390}