1use std::{borrow::BorrowMut, collections::VecDeque};
2
3use gix_object::{FindExt, TreeRefIter, tree::EntryRef};
4
5use crate::tree::{
6 Error, State, TreeInfoTuple, Visit,
7 visit::{Change, ChangeId, Relation},
8};
9
10#[doc(alias = "diff_tree_to_tree", alias = "git2")]
31pub fn diff<StateMut>(
32 lhs: TreeRefIter<'_>,
33 rhs: TreeRefIter<'_>,
34 mut state: StateMut,
35 objects: impl gix_object::Find,
36 delegate: &mut impl Visit,
37) -> Result<(), Error>
38where
39 StateMut: BorrowMut<State>,
40{
41 let state = state.borrow_mut();
42 state.clear();
43 let mut lhs_entries = peekable(lhs);
44 let mut rhs_entries = peekable(rhs);
45 let mut relation = None;
46 let mut pop_path = false;
47
48 loop {
49 if pop_path {
50 delegate.pop_path_component();
51 }
52 pop_path = true;
53
54 match (lhs_entries.next(), rhs_entries.next()) {
55 (None, None) => {
56 match state.trees.pop_front() {
57 Some((None, Some(rhs), relation_to_propagate)) => {
58 delegate.pop_front_tracked_path_and_set_current();
59 relation = relation_to_propagate;
60 rhs_entries = peekable(objects.find_tree_iter(&rhs, &mut state.buf2)?);
61 }
62 Some((Some(lhs), Some(rhs), relation_to_propagate)) => {
63 delegate.pop_front_tracked_path_and_set_current();
64 lhs_entries = peekable(objects.find_tree_iter(&lhs, &mut state.buf1)?);
65 rhs_entries = peekable(objects.find_tree_iter(&rhs, &mut state.buf2)?);
66 relation = relation_to_propagate;
67 }
68 Some((Some(lhs), None, relation_to_propagate)) => {
69 delegate.pop_front_tracked_path_and_set_current();
70 lhs_entries = peekable(objects.find_tree_iter(&lhs, &mut state.buf1)?);
71 relation = relation_to_propagate;
72 }
73 Some((None, None, _)) => unreachable!("BUG: it makes no sense to fill the stack with empties"),
74 None => return Ok(()),
75 }
76 pop_path = false;
77 }
78 (Some(lhs), Some(rhs)) => {
79 use std::cmp::Ordering::*;
80 let (lhs, rhs) = (lhs?, rhs?);
81 match compare(&lhs, &rhs) {
82 Equal => handle_lhs_and_rhs_with_equal_filenames(
83 lhs,
84 rhs,
85 &mut state.trees,
86 &mut state.change_id,
87 relation,
88 delegate,
89 )?,
90 Less => catchup_lhs_with_rhs(
91 &mut lhs_entries,
92 lhs,
93 rhs,
94 &mut state.trees,
95 &mut state.change_id,
96 relation,
97 delegate,
98 )?,
99 Greater => catchup_rhs_with_lhs(
100 &mut rhs_entries,
101 lhs,
102 rhs,
103 &mut state.trees,
104 &mut state.change_id,
105 relation,
106 delegate,
107 )?,
108 }
109 }
110 (Some(lhs), None) => {
111 let lhs = lhs?;
112 delete_entry_schedule_recursion(lhs, &mut state.trees, &mut state.change_id, relation, delegate)?;
113 }
114 (None, Some(rhs)) => {
115 let rhs = rhs?;
116 add_entry_schedule_recursion(rhs, &mut state.trees, &mut state.change_id, relation, delegate)?;
117 }
118 }
119 }
120}
121
122fn compare(a: &EntryRef<'_>, b: &EntryRef<'_>) -> std::cmp::Ordering {
123 gix_object::tree::name_order(a.filename, a.mode.is_tree(), b.filename, b.mode.is_tree())
124}
125
126fn delete_entry_schedule_recursion(
127 entry: EntryRef<'_>,
128 queue: &mut VecDeque<TreeInfoTuple>,
129 change_id: &mut ChangeId,
130 relation_to_propagate: Option<Relation>,
131 delegate: &mut impl Visit,
132) -> Result<(), Error> {
133 delegate.push_path_component(entry.filename);
134 let relation = relation_to_propagate.or_else(|| {
135 entry.mode.is_tree().then(|| {
136 *change_id += 1;
137 Relation::Parent(*change_id)
138 })
139 });
140 let is_cancelled = delegate
141 .visit(Change::Deletion {
142 entry_mode: entry.mode,
143 oid: entry.oid.to_owned(),
144 relation,
145 })
146 .is_break();
147 if is_cancelled {
148 return Err(Error::Cancelled);
149 }
150 if entry.mode.is_tree() {
151 delegate.pop_path_component();
152 delegate.push_back_tracked_path_component(entry.filename);
153 queue.push_back((Some(entry.oid.to_owned()), None, to_child(relation)));
154 }
155 Ok(())
156}
157
158fn add_entry_schedule_recursion(
159 entry: EntryRef<'_>,
160 queue: &mut VecDeque<TreeInfoTuple>,
161 change_id: &mut ChangeId,
162 relation_to_propagate: Option<Relation>,
163 delegate: &mut impl Visit,
164) -> Result<(), Error> {
165 delegate.push_path_component(entry.filename);
166 let relation = relation_to_propagate.or_else(|| {
167 entry.mode.is_tree().then(|| {
168 *change_id += 1;
169 Relation::Parent(*change_id)
170 })
171 });
172 if delegate
173 .visit(Change::Addition {
174 entry_mode: entry.mode,
175 oid: entry.oid.to_owned(),
176 relation,
177 })
178 .is_break()
179 {
180 return Err(Error::Cancelled);
181 }
182 if entry.mode.is_tree() {
183 delegate.pop_path_component();
184 delegate.push_back_tracked_path_component(entry.filename);
185 queue.push_back((None, Some(entry.oid.to_owned()), to_child(relation)));
186 }
187 Ok(())
188}
189
190fn catchup_rhs_with_lhs(
191 rhs_entries: &mut IteratorType<TreeRefIter<'_>>,
192 lhs: EntryRef<'_>,
193 rhs: EntryRef<'_>,
194 queue: &mut VecDeque<TreeInfoTuple>,
195 change_id: &mut ChangeId,
196 relation_to_propagate: Option<Relation>,
197 delegate: &mut impl Visit,
198) -> Result<(), Error> {
199 use std::cmp::Ordering::*;
200 add_entry_schedule_recursion(rhs, queue, change_id, relation_to_propagate, delegate)?;
201 loop {
202 match rhs_entries.peek() {
203 Some(Ok(rhs)) => match compare(&lhs, rhs) {
204 Equal => {
205 let rhs = rhs_entries.next().transpose()?.expect("the peeked item to be present");
206 delegate.pop_path_component();
207 handle_lhs_and_rhs_with_equal_filenames(
208 lhs,
209 rhs,
210 queue,
211 change_id,
212 relation_to_propagate,
213 delegate,
214 )?;
215 break;
216 }
217 Greater => {
218 let rhs = rhs_entries.next().transpose()?.expect("the peeked item to be present");
219 delegate.pop_path_component();
220 add_entry_schedule_recursion(rhs, queue, change_id, relation_to_propagate, delegate)?;
221 }
222 Less => {
223 delegate.pop_path_component();
224 delete_entry_schedule_recursion(lhs, queue, change_id, relation_to_propagate, delegate)?;
225 break;
226 }
227 },
228 Some(Err(_)) => {
229 let Err(err) = rhs_entries.next().expect("the peeked item to be present") else {
230 unreachable!("peeked error changed before it was consumed")
231 };
232 return Err(err.into());
233 }
234 None => {
235 delegate.pop_path_component();
236 delete_entry_schedule_recursion(lhs, queue, change_id, relation_to_propagate, delegate)?;
237 break;
238 }
239 }
240 }
241 Ok(())
242}
243
244fn catchup_lhs_with_rhs(
245 lhs_entries: &mut IteratorType<TreeRefIter<'_>>,
246 lhs: EntryRef<'_>,
247 rhs: EntryRef<'_>,
248 queue: &mut VecDeque<TreeInfoTuple>,
249 change_id: &mut ChangeId,
250 relation_to_propagate: Option<Relation>,
251 delegate: &mut impl Visit,
252) -> Result<(), Error> {
253 use std::cmp::Ordering::*;
254 delete_entry_schedule_recursion(lhs, queue, change_id, relation_to_propagate, delegate)?;
255 loop {
256 match lhs_entries.peek() {
257 Some(Ok(lhs)) => match compare(lhs, &rhs) {
258 Equal => {
259 let lhs = lhs_entries.next().expect("the peeked item to be present")?;
260 delegate.pop_path_component();
261 handle_lhs_and_rhs_with_equal_filenames(
262 lhs,
263 rhs,
264 queue,
265 change_id,
266 relation_to_propagate,
267 delegate,
268 )?;
269 break;
270 }
271 Less => {
272 let lhs = lhs_entries.next().expect("the peeked item to be present")?;
273 delegate.pop_path_component();
274 delete_entry_schedule_recursion(lhs, queue, change_id, relation_to_propagate, delegate)?;
275 }
276 Greater => {
277 delegate.pop_path_component();
278 add_entry_schedule_recursion(rhs, queue, change_id, relation_to_propagate, delegate)?;
279 break;
280 }
281 },
282 Some(Err(_)) => {
283 let Err(err) = lhs_entries.next().expect("the peeked item to be present") else {
284 unreachable!("peeked error changed before it was consumed")
285 };
286 return Err(err.into());
287 }
288 None => {
289 delegate.pop_path_component();
290 add_entry_schedule_recursion(rhs, queue, change_id, relation_to_propagate, delegate)?;
291 break;
292 }
293 }
294 }
295 Ok(())
296}
297
298fn handle_lhs_and_rhs_with_equal_filenames(
299 lhs: EntryRef<'_>,
300 rhs: EntryRef<'_>,
301 queue: &mut VecDeque<TreeInfoTuple>,
302 change_id: &mut ChangeId,
303 relation_to_propagate: Option<Relation>,
304 delegate: &mut impl Visit,
305) -> Result<(), Error> {
306 match (lhs.mode.is_tree(), rhs.mode.is_tree()) {
307 (true, true) => {
308 if lhs.oid == rhs.oid {
309 delegate.push_path_component(lhs.filename);
313 } else {
314 delegate.push_back_tracked_path_component(lhs.filename);
315 if delegate
316 .visit(Change::Modification {
317 previous_entry_mode: lhs.mode,
318 previous_oid: lhs.oid.to_owned(),
319 entry_mode: rhs.mode,
320 oid: rhs.oid.to_owned(),
321 })
322 .is_break()
323 {
324 return Err(Error::Cancelled);
325 }
326 queue.push_back((
327 Some(lhs.oid.to_owned()),
328 Some(rhs.oid.to_owned()),
329 relation_to_propagate,
330 ));
331 }
332 }
333 (_, true) => {
334 delegate.push_back_tracked_path_component(lhs.filename);
335 if delegate
336 .visit(Change::Deletion {
337 entry_mode: lhs.mode,
338 oid: lhs.oid.to_owned(),
339 relation: None,
340 })
341 .is_break()
342 {
343 return Err(Error::Cancelled);
344 }
345
346 let relation = relation_to_propagate.or_else(|| {
347 *change_id += 1;
348 Some(Relation::Parent(*change_id))
349 });
350 if delegate
351 .visit(Change::Addition {
352 entry_mode: rhs.mode,
353 oid: rhs.oid.to_owned(),
354 relation,
355 })
356 .is_break()
357 {
358 return Err(Error::Cancelled);
359 }
360 queue.push_back((None, Some(rhs.oid.to_owned()), to_child(relation)));
361 }
362 (true, _) => {
363 delegate.push_back_tracked_path_component(lhs.filename);
364 let relation = relation_to_propagate.or_else(|| {
365 *change_id += 1;
366 Some(Relation::Parent(*change_id))
367 });
368 if delegate
369 .visit(Change::Deletion {
370 entry_mode: lhs.mode,
371 oid: lhs.oid.to_owned(),
372 relation,
373 })
374 .is_break()
375 {
376 return Err(Error::Cancelled);
377 }
378 if delegate
379 .visit(Change::Addition {
380 entry_mode: rhs.mode,
381 oid: rhs.oid.to_owned(),
382 relation: None,
383 })
384 .is_break()
385 {
386 return Err(Error::Cancelled);
387 }
388 queue.push_back((Some(lhs.oid.to_owned()), None, to_child(relation)));
389 }
390 (false, false) => {
391 delegate.push_path_component(lhs.filename);
392 debug_assert!(lhs.mode.is_no_tree() && lhs.mode.is_no_tree());
393 if (lhs.oid != rhs.oid || lhs.mode != rhs.mode)
394 && delegate
395 .visit(Change::Modification {
396 previous_entry_mode: lhs.mode,
397 previous_oid: lhs.oid.to_owned(),
398 entry_mode: rhs.mode,
399 oid: rhs.oid.to_owned(),
400 })
401 .is_break()
402 {
403 return Err(Error::Cancelled);
404 }
405 }
406 }
407 Ok(())
408}
409
410type IteratorType<I> = std::iter::Peekable<I>;
411
412fn to_child(r: Option<Relation>) -> Option<Relation> {
413 r.map(|r| match r {
414 Relation::Parent(id) => Relation::ChildOfParent(id),
415 Relation::ChildOfParent(id) => Relation::ChildOfParent(id),
416 })
417}
418
419fn peekable<I: Iterator>(iter: I) -> IteratorType<I> {
420 iter.peekable()
421}
422
423#[cfg(test)]
424mod tests {
425 use std::cmp::Ordering;
426
427 use gix_object::tree::EntryKind;
428
429 use super::*;
430
431 #[test]
432 fn compare_select_samples() {
433 let null = gix_testtools::object_hash().null();
434 let actual = compare(
435 &EntryRef {
436 mode: EntryKind::Blob.into(),
437 filename: "plumbing-cli.rs".into(),
438 oid: &null,
439 },
440 &EntryRef {
441 mode: EntryKind::Tree.into(),
442 filename: "plumbing".into(),
443 oid: &null,
444 },
445 );
446 assert_eq!(actual, Ordering::Less);
447 let actual = compare(
448 &EntryRef {
449 mode: EntryKind::Tree.into(),
450 filename: "plumbing-cli.rs".into(),
451 oid: &null,
452 },
453 &EntryRef {
454 mode: EntryKind::Blob.into(),
455 filename: "plumbing".into(),
456 oid: &null,
457 },
458 );
459 assert_eq!(actual, Ordering::Greater);
460 }
461}