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