| @@ -0,0 +1,364 @@ |
| 1 | +use crate::{ |
| 2 | + tree, |
| 3 | + tree::{changes::Error::Cancelled, visit::Change, TreeInfo, TreeInfoPair}, |
| 4 | +}; |
| 5 | +use git_hash::{oid, ObjectId}; |
| 6 | +use git_object::immutable; |
| 7 | +use quick_error::quick_error; |
| 8 | +use std::{borrow::BorrowMut, collections::VecDeque}; |
| 9 | + |
| 10 | +quick_error! { |
| 11 | + #[derive(Debug)] |
| 12 | + pub enum Error { |
| 13 | + NotFound(oid: ObjectId) { |
| 14 | + display("The object {} referenced by the tree was not found in the database", oid) |
| 15 | + } |
| 16 | + Cancelled { |
| 17 | + display("The delegate cancelled the operation") |
| 18 | + } |
| 19 | + EntriesDecode(err: immutable::object::decode::Error) { |
| 20 | + display("tree entries could not be decoded.") |
| 21 | + from() |
| 22 | + source(err) |
| 23 | + } |
| 24 | + } |
| 25 | +} |
| 26 | + |
| 27 | +impl<'a> tree::Changes<'a> { |
| 28 | + /// that need to be applied to `self` to get `other`. |
| 29 | + /// TODO: Talk about choices in error handling and why Option<T> is enough, as opposed to the usual Result<Option<T>> |
| 30 | + /// Talk about progress (done via delegate if needed) |
| 31 | + /// |
| 32 | + /// # Notes |
| 33 | + /// |
| 34 | + /// * Tree entries are expected to be ordered using [`tree-entry-comparison`][git_cmp_c] (the same [in Rust][git_cmp_rs]) |
| 35 | + /// |
| 36 | + /// [git_cmp_c]: https://github.com/git/git/blob/311531c9de557d25ac087c1637818bd2aad6eb3a/tree-diff.c#L49:L65 |
| 37 | + /// [git_cmp_rs]: https://github.com/Byron/gitoxide/blob/a4d5f99c8dc99bf814790928a3bf9649cd99486b/git-object/src/mutable/tree.rs#L52-L55 |
| 38 | + /// |
| 39 | + /// * it does a breadth first iteration as buffer space only fits two trees, the current one on the one we compare with. |
| 40 | + /// * does not do rename tracking but attempts to reduce allocations to zero (so performance is mostly determined |
| 41 | + /// by the delegate implementation which should be as specific as possible. |
| 42 | + /// * cycle checking is not performed, but can be performed in the delegate |
| 43 | + /// * [std::mem::ManuallyDrop] is used because `Peekable` is needed. When using it as wrapper around our no-drop iterators, all of the sudden |
| 44 | + /// borrowcheck complains as Drop is present (even though it's not) |
| 45 | + pub fn needed_to_obtain<LocateFn, R, StateMut>( |
| 46 | + mut self, |
| 47 | + other: immutable::TreeIter<'a>, |
| 48 | + mut state: StateMut, |
| 49 | + mut locate: LocateFn, |
| 50 | + delegate: &mut R, |
| 51 | + ) -> Result<(), Error> |
| 52 | + where |
| 53 | + LocateFn: for<'b> FnMut(&oid, &'b mut Vec<u8>) -> Option<immutable::tree::TreeIter<'b>>, |
| 54 | + R: tree::Visit, |
| 55 | + StateMut: BorrowMut<tree::State<R::PathId>>, |
| 56 | + { |
| 57 | + let state = state.borrow_mut(); |
| 58 | + state.clear(); |
| 59 | + let mut lhs_entries = peekable(self.0.take().unwrap_or_default()); |
| 60 | + let mut rhs_entries = peekable(other); |
| 61 | + let mut avoid_popping_path: Option<()> = None; |
| 62 | + |
| 63 | + loop { |
| 64 | + if avoid_popping_path.take().is_none() { |
| 65 | + delegate.pop_path_component(); |
| 66 | + } |
| 67 | + match (lhs_entries.next(), rhs_entries.next()) { |
| 68 | + (None, None) => { |
| 69 | + match state.trees.pop_front() { |
| 70 | + Some((None, Some(rhs))) => { |
| 71 | + delegate.set_current_path(rhs.parent_path_id.clone()); |
| 72 | + rhs_entries = |
| 73 | + peekable(locate(&rhs.tree_id, &mut state.buf2).ok_or(Error::NotFound(rhs.tree_id))?); |
| 74 | + } |
| 75 | + Some((Some(lhs), Some(rhs))) => { |
| 76 | + delegate.set_current_path(lhs.parent_path_id.clone()); |
| 77 | + lhs_entries = |
| 78 | + peekable(locate(&lhs.tree_id, &mut state.buf1).ok_or(Error::NotFound(lhs.tree_id))?); |
| 79 | + rhs_entries = |
| 80 | + peekable(locate(&rhs.tree_id, &mut state.buf2).ok_or(Error::NotFound(rhs.tree_id))?); |
| 81 | + } |
| 82 | + Some((Some(lhs), None)) => { |
| 83 | + delegate.set_current_path(lhs.parent_path_id.clone()); |
| 84 | + lhs_entries = |
| 85 | + peekable(locate(&lhs.tree_id, &mut state.buf1).ok_or(Error::NotFound(lhs.tree_id))?); |
| 86 | + } |
| 87 | + Some((None, None)) => unreachable!("BUG: it makes no sense to fill the stack with empties"), |
| 88 | + None => return Ok(()), |
| 89 | + }; |
| 90 | + avoid_popping_path = Some(()); |
| 91 | + } |
| 92 | + (Some(lhs), Some(rhs)) => { |
| 93 | + use std::cmp::Ordering::*; |
| 94 | + let (lhs, rhs) = (lhs?, rhs?); |
| 95 | + match lhs.filename.cmp(rhs.filename) { |
| 96 | + Equal => handle_lhs_and_rhs_with_equal_filenames(lhs, rhs, &mut state.trees, delegate)?, |
| 97 | + Less => catchup_lhs_with_rhs(&mut lhs_entries, lhs, rhs, &mut state.trees, delegate)?, |
| 98 | + Greater => catchup_rhs_with_lhs(&mut rhs_entries, lhs, rhs, &mut state.trees, delegate)?, |
| 99 | + } |
| 100 | + } |
| 101 | + (Some(lhs), None) => { |
| 102 | + let lhs = lhs?; |
| 103 | + delete_entry_schedule_recursion(lhs, &mut state.trees, delegate)?; |
| 104 | + } |
| 105 | + (None, Some(rhs)) => { |
| 106 | + let rhs = rhs?; |
| 107 | + add_entry_schedule_recursion(rhs, &mut state.trees, delegate)?; |
| 108 | + } |
| 109 | + } |
| 110 | + } |
| 111 | + } |
| 112 | +} |
| 113 | + |
| 114 | +fn delete_entry_schedule_recursion<R: tree::Visit>( |
| 115 | + entry: immutable::tree::Entry<'_>, |
| 116 | + queue: &mut VecDeque<TreeInfoPair<R::PathId>>, |
| 117 | + delegate: &mut R, |
| 118 | +) -> Result<(), Error> { |
| 119 | + delegate.push_path_component(entry.filename); |
| 120 | + if delegate |
| 121 | + .visit(Change::Deletion { |
| 122 | + entry_mode: entry.mode, |
| 123 | + oid: entry.oid.to_owned(), |
| 124 | + }) |
| 125 | + .cancelled() |
| 126 | + { |
| 127 | + return Err(Error::Cancelled); |
| 128 | + } |
| 129 | + if entry.mode.is_tree() { |
| 130 | + delegate.pop_path_component(); |
| 131 | + let path_id = delegate.push_tracked_path_component(entry.filename); |
| 132 | + queue.push_back(( |
| 133 | + Some(TreeInfo { |
| 134 | + tree_id: entry.oid.to_owned(), |
| 135 | + parent_path_id: path_id, |
| 136 | + }), |
| 137 | + None, |
| 138 | + )); |
| 139 | + } |
| 140 | + Ok(()) |
| 141 | +} |
| 142 | + |
| 143 | +fn add_entry_schedule_recursion<R: tree::Visit>( |
| 144 | + entry: immutable::tree::Entry<'_>, |
| 145 | + queue: &mut VecDeque<TreeInfoPair<R::PathId>>, |
| 146 | + delegate: &mut R, |
| 147 | +) -> Result<(), Error> { |
| 148 | + delegate.push_path_component(entry.filename); |
| 149 | + if delegate |
| 150 | + .visit(Change::Addition { |
| 151 | + entry_mode: entry.mode, |
| 152 | + oid: entry.oid.to_owned(), |
| 153 | + }) |
| 154 | + .cancelled() |
| 155 | + { |
| 156 | + return Err(Error::Cancelled); |
| 157 | + } |
| 158 | + if entry.mode.is_tree() { |
| 159 | + delegate.pop_path_component(); |
| 160 | + let path_id = delegate.push_tracked_path_component(entry.filename); |
| 161 | + queue.push_back(( |
| 162 | + None, |
| 163 | + Some(TreeInfo { |
| 164 | + tree_id: entry.oid.to_owned(), |
| 165 | + parent_path_id: path_id, |
| 166 | + }), |
| 167 | + )) |
| 168 | + } |
| 169 | + Ok(()) |
| 170 | +} |
| 171 | +fn catchup_rhs_with_lhs<R: tree::Visit>( |
| 172 | + rhs_entries: &mut IteratorType<immutable::TreeIter<'_>>, |
| 173 | + lhs: immutable::tree::Entry<'_>, |
| 174 | + rhs: immutable::tree::Entry<'_>, |
| 175 | + queue: &mut VecDeque<TreeInfoPair<R::PathId>>, |
| 176 | + delegate: &mut R, |
| 177 | +) -> Result<(), Error> { |
| 178 | + use std::cmp::Ordering::*; |
| 179 | + add_entry_schedule_recursion(rhs, queue, delegate)?; |
| 180 | + loop { |
| 181 | + match rhs_entries.peek() { |
| 182 | + Some(Ok(rhs)) => match lhs.filename.cmp(rhs.filename) { |
| 183 | + Equal => { |
| 184 | + let rhs = rhs_entries.next().transpose()?.expect("the peeked item tobe present"); |
| 185 | + delegate.pop_path_component(); |
| 186 | + handle_lhs_and_rhs_with_equal_filenames(lhs, rhs, queue, delegate)?; |
| 187 | + break; |
| 188 | + } |
| 189 | + Greater => { |
| 190 | + let rhs = rhs_entries.next().transpose()?.expect("the peeked item tobe present"); |
| 191 | + delegate.pop_path_component(); |
| 192 | + add_entry_schedule_recursion(rhs, queue, delegate)?; |
| 193 | + } |
| 194 | + Less => { |
| 195 | + delegate.pop_path_component(); |
| 196 | + delete_entry_schedule_recursion(lhs, queue, delegate)?; |
| 197 | + break; |
| 198 | + } |
| 199 | + }, |
| 200 | + Some(Err(err)) => return Err(Error::EntriesDecode(err.to_owned())), |
| 201 | + None => { |
| 202 | + delegate.pop_path_component(); |
| 203 | + delete_entry_schedule_recursion(lhs, queue, delegate)?; |
| 204 | + break; |
| 205 | + } |
| 206 | + } |
| 207 | + } |
| 208 | + Ok(()) |
| 209 | +} |
| 210 | + |
| 211 | +fn catchup_lhs_with_rhs<R: tree::Visit>( |
| 212 | + lhs_entries: &mut IteratorType<immutable::TreeIter<'_>>, |
| 213 | + lhs: immutable::tree::Entry<'_>, |
| 214 | + rhs: immutable::tree::Entry<'_>, |
| 215 | + queue: &mut VecDeque<TreeInfoPair<R::PathId>>, |
| 216 | + delegate: &mut R, |
| 217 | +) -> Result<(), Error> { |
| 218 | + use std::cmp::Ordering::*; |
| 219 | + delete_entry_schedule_recursion(lhs, queue, delegate)?; |
| 220 | + loop { |
| 221 | + match lhs_entries.peek() { |
| 222 | + Some(Ok(lhs)) => match lhs.filename.cmp(rhs.filename) { |
| 223 | + Equal => { |
| 224 | + let lhs = lhs_entries.next().expect("the peeked item to be present")?; |
| 225 | + delegate.pop_path_component(); |
| 226 | + handle_lhs_and_rhs_with_equal_filenames(lhs, rhs, queue, delegate)?; |
| 227 | + break; |
| 228 | + } |
| 229 | + Less => { |
| 230 | + let lhs = lhs_entries.next().expect("the peeked item to be present")?; |
| 231 | + delegate.pop_path_component(); |
| 232 | + delete_entry_schedule_recursion(lhs, queue, delegate)?; |
| 233 | + } |
| 234 | + Greater => { |
| 235 | + delegate.pop_path_component(); |
| 236 | + add_entry_schedule_recursion(rhs, queue, delegate)?; |
| 237 | + break; |
| 238 | + } |
| 239 | + }, |
| 240 | + Some(Err(err)) => return Err(Error::EntriesDecode(err.to_owned())), |
| 241 | + None => { |
| 242 | + delegate.pop_path_component(); |
| 243 | + add_entry_schedule_recursion(rhs, queue, delegate)?; |
| 244 | + break; |
| 245 | + } |
| 246 | + } |
| 247 | + } |
| 248 | + Ok(()) |
| 249 | +} |
| 250 | + |
| 251 | +fn handle_lhs_and_rhs_with_equal_filenames<R: tree::Visit>( |
| 252 | + lhs: immutable::tree::Entry<'_>, |
| 253 | + rhs: immutable::tree::Entry<'_>, |
| 254 | + queue: &mut VecDeque<TreeInfoPair<R::PathId>>, |
| 255 | + delegate: &mut R, |
| 256 | +) -> Result<(), Error> { |
| 257 | + use git_object::tree::EntryMode::*; |
| 258 | + match (lhs.mode, rhs.mode) { |
| 259 | + (Tree, Tree) => { |
| 260 | + let path_id = delegate.push_tracked_path_component(lhs.filename); |
| 261 | + if lhs.oid != rhs.oid |
| 262 | + && delegate |
| 263 | + .visit(Change::Modification { |
| 264 | + previous_entry_mode: lhs.mode, |
| 265 | + previous_oid: lhs.oid.to_owned(), |
| 266 | + entry_mode: rhs.mode, |
| 267 | + oid: rhs.oid.to_owned(), |
| 268 | + }) |
| 269 | + .cancelled() |
| 270 | + { |
| 271 | + return Err(Cancelled); |
| 272 | + } |
| 273 | + queue.push_back(( |
| 274 | + Some(TreeInfo { |
| 275 | + tree_id: lhs.oid.to_owned(), |
| 276 | + parent_path_id: path_id.clone(), |
| 277 | + }), |
| 278 | + Some(TreeInfo { |
| 279 | + tree_id: rhs.oid.to_owned(), |
| 280 | + parent_path_id: path_id, |
| 281 | + }), |
| 282 | + )); |
| 283 | + } |
| 284 | + (lhs_mode, Tree) if lhs_mode.is_no_tree() => { |
| 285 | + let path_id = delegate.push_tracked_path_component(lhs.filename); |
| 286 | + if delegate |
| 287 | + .visit(Change::Deletion { |
| 288 | + entry_mode: lhs.mode, |
| 289 | + oid: lhs.oid.to_owned(), |
| 290 | + }) |
| 291 | + .cancelled() |
| 292 | + { |
| 293 | + return Err(Cancelled); |
| 294 | + }; |
| 295 | + if delegate |
| 296 | + .visit(Change::Addition { |
| 297 | + entry_mode: rhs.mode, |
| 298 | + oid: rhs.oid.to_owned(), |
| 299 | + }) |
| 300 | + .cancelled() |
| 301 | + { |
| 302 | + return Err(Cancelled); |
| 303 | + }; |
| 304 | + queue.push_back(( |
| 305 | + None, |
| 306 | + Some(TreeInfo { |
| 307 | + tree_id: rhs.oid.to_owned(), |
| 308 | + parent_path_id: path_id, |
| 309 | + }), |
| 310 | + )); |
| 311 | + } |
| 312 | + (Tree, rhs_mode) if rhs_mode.is_no_tree() => { |
| 313 | + let path_id = delegate.push_tracked_path_component(lhs.filename); |
| 314 | + if delegate |
| 315 | + .visit(Change::Deletion { |
| 316 | + entry_mode: lhs.mode, |
| 317 | + oid: lhs.oid.to_owned(), |
| 318 | + }) |
| 319 | + .cancelled() |
| 320 | + { |
| 321 | + return Err(Error::Cancelled); |
| 322 | + } |
| 323 | + if delegate |
| 324 | + .visit(Change::Addition { |
| 325 | + entry_mode: rhs.mode, |
| 326 | + oid: rhs.oid.to_owned(), |
| 327 | + }) |
| 328 | + .cancelled() |
| 329 | + { |
| 330 | + return Err(Cancelled); |
| 331 | + }; |
| 332 | + queue.push_back(( |
| 333 | + Some(TreeInfo { |
| 334 | + tree_id: lhs.oid.to_owned(), |
| 335 | + parent_path_id: path_id, |
| 336 | + }), |
| 337 | + None, |
| 338 | + )); |
| 339 | + } |
| 340 | + (lhs_non_tree, rhs_non_tree) => { |
| 341 | + delegate.push_path_component(lhs.filename); |
| 342 | + debug_assert!(lhs_non_tree.is_no_tree() && rhs_non_tree.is_no_tree()); |
| 343 | + if lhs.oid != rhs.oid |
| 344 | + && delegate |
| 345 | + .visit(Change::Modification { |
| 346 | + previous_entry_mode: lhs.mode, |
| 347 | + previous_oid: lhs.oid.to_owned(), |
| 348 | + entry_mode: rhs.mode, |
| 349 | + oid: rhs.oid.to_owned(), |
| 350 | + }) |
| 351 | + .cancelled() |
| 352 | + { |
| 353 | + return Err(Error::Cancelled); |
| 354 | + } |
| 355 | + } |
| 356 | + }; |
| 357 | + Ok(()) |
| 358 | +} |
| 359 | + |
| 360 | +type IteratorType<I> = std::mem::ManuallyDrop<std::iter::Peekable<I>>; |
| 361 | + |
| 362 | +fn peekable<I: Iterator>(iter: I) -> IteratorType<I> { |
| 363 | + std::mem::ManuallyDrop::new(iter.peekable()) |
| 364 | +} |