| @@ -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 | } |