heddle_object_model/object/
tree_diff.rs1use std::ops::ControlFlow;
8
9#[cfg(feature = "async-source")]
10use super::AsyncObjectSource;
11use super::{ContentHash, DiffKind, FileChange, FileChangeSet, ObjectSource, Tree};
12
13struct DiffFrame {
14 from: Option<Tree>,
15 to: Option<Tree>,
16 prefix: String,
17 from_index: usize,
18 to_index: usize,
19}
20
21enum DiffStep {
22 Emit(FileChange),
23 Descend {
24 from_hash: Option<ContentHash>,
25 to_hash: Option<ContentHash>,
26 name: String,
27 },
28 Done,
29}
30
31fn advance_merge(frame: &mut DiffFrame) -> DiffStep {
32 let from_entries = frame.from.as_ref().map_or(&[][..], Tree::entries);
33 let to_entries = frame.to.as_ref().map_or(&[][..], Tree::entries);
34
35 loop {
36 match (
37 from_entries.get(frame.from_index),
38 to_entries.get(frame.to_index),
39 ) {
40 (Some(from_entry), Some(to_entry)) => match from_entry.name().cmp(to_entry.name()) {
41 std::cmp::Ordering::Less => {
42 frame.from_index += 1;
43 if let Some(from_hash) = from_entry.tree_hash() {
44 return DiffStep::Descend {
45 from_hash: Some(from_hash),
46 to_hash: None,
47 name: from_entry.name().to_owned(),
48 };
49 }
50 return DiffStep::Emit(FileChange::new(
51 child_path(&frame.prefix, from_entry.name()),
52 DiffKind::Deleted,
53 ));
54 }
55 std::cmp::Ordering::Greater => {
56 frame.to_index += 1;
57 if let Some(to_hash) = to_entry.tree_hash() {
58 return DiffStep::Descend {
59 from_hash: None,
60 to_hash: Some(to_hash),
61 name: to_entry.name().to_owned(),
62 };
63 }
64 return DiffStep::Emit(FileChange::new(
65 child_path(&frame.prefix, to_entry.name()),
66 DiffKind::Added,
67 ));
68 }
69 std::cmp::Ordering::Equal => {
70 frame.from_index += 1;
71 frame.to_index += 1;
72 if from_entry.target() == to_entry.target() {
73 continue;
74 }
75 if let (Some(from_hash), Some(to_hash)) =
76 (from_entry.tree_hash(), to_entry.tree_hash())
77 {
78 return DiffStep::Descend {
79 from_hash: Some(from_hash),
80 to_hash: Some(to_hash),
81 name: to_entry.name().to_owned(),
82 };
83 }
84 return DiffStep::Emit(FileChange::new(
85 child_path(&frame.prefix, to_entry.name()),
86 DiffKind::Modified,
87 ));
88 }
89 },
90 (Some(from_entry), None) => {
91 frame.from_index += 1;
92 if let Some(from_hash) = from_entry.tree_hash() {
93 return DiffStep::Descend {
94 from_hash: Some(from_hash),
95 to_hash: None,
96 name: from_entry.name().to_owned(),
97 };
98 }
99 return DiffStep::Emit(FileChange::new(
100 child_path(&frame.prefix, from_entry.name()),
101 DiffKind::Deleted,
102 ));
103 }
104 (None, Some(to_entry)) => {
105 frame.to_index += 1;
106 if let Some(to_hash) = to_entry.tree_hash() {
107 return DiffStep::Descend {
108 from_hash: None,
109 to_hash: Some(to_hash),
110 name: to_entry.name().to_owned(),
111 };
112 }
113 return DiffStep::Emit(FileChange::new(
114 child_path(&frame.prefix, to_entry.name()),
115 DiffKind::Added,
116 ));
117 }
118 (None, None) => return DiffStep::Done,
119 }
120 }
121}
122
123pub fn diff_trees<S: ObjectSource + ?Sized>(
130 store: &S,
131 from: &crate::object::ContentHash,
132 to: &crate::object::ContentHash,
133) -> Result<FileChangeSet, anyhow::Error> {
134 let mut changes = FileChangeSet::new();
135 let _ = diff_trees_visit(store, from, to, |change| {
138 changes.push(change);
139 ControlFlow::<()>::Continue(())
140 })?;
141 Ok(changes)
142}
143
144pub fn diff_trees_visit<S, V, B>(
159 store: &S,
160 from: &crate::object::ContentHash,
161 to: &crate::object::ContentHash,
162 mut visitor: V,
163) -> Result<ControlFlow<B>, anyhow::Error>
164where
165 S: ObjectSource + ?Sized,
166 V: FnMut(FileChange) -> ControlFlow<B>,
167{
168 if from == to {
169 return Ok(ControlFlow::Continue(()));
170 }
171 let from_tree = store.get_tree(from)?;
172 let to_tree = store.get_tree(to)?;
173 let mut stack = vec![DiffFrame {
174 from: from_tree,
175 to: to_tree,
176 prefix: String::new(),
177 from_index: 0,
178 to_index: 0,
179 }];
180
181 while !stack.is_empty() {
182 match advance_merge(stack.last_mut().expect("stack is not empty")) {
183 DiffStep::Emit(change) => {
184 if let ControlFlow::Break(b) = visitor(change) {
185 return Ok(ControlFlow::Break(b));
186 }
187 }
188 DiffStep::Descend {
189 from_hash,
190 to_hash,
191 name,
192 } => {
193 let from_subtree = from_hash
194 .map(|hash| store.get_tree(&hash))
195 .transpose()?
196 .flatten();
197 let to_subtree = to_hash
198 .map(|hash| store.get_tree(&hash))
199 .transpose()?
200 .flatten();
201 let prefix = child_path(
202 &stack.last().expect("parent frame remains on stack").prefix,
203 &name,
204 );
205 stack.push(DiffFrame {
206 from: from_subtree,
207 to: to_subtree,
208 prefix,
209 from_index: 0,
210 to_index: 0,
211 });
212 }
213 DiffStep::Done => {
214 stack.pop();
215 }
216 }
217 }
218
219 Ok(ControlFlow::Continue(()))
220}
221
222#[cfg(feature = "async-source")]
223pub async fn diff_trees_visit_async<S, V, B>(
224 store: &S,
225 from: &crate::object::ContentHash,
226 to: &crate::object::ContentHash,
227 mut visitor: V,
228) -> Result<ControlFlow<B>, anyhow::Error>
229where
230 S: AsyncObjectSource + Sync + ?Sized,
231 V: FnMut(FileChange) -> ControlFlow<B> + Send,
232 B: Send,
233{
234 if from == to {
235 return Ok(ControlFlow::Continue(()));
236 }
237 let from_tree = store.get_tree(from).await?;
238 let to_tree = store.get_tree(to).await?;
239 let mut stack = vec![DiffFrame {
240 from: from_tree,
241 to: to_tree,
242 prefix: String::new(),
243 from_index: 0,
244 to_index: 0,
245 }];
246
247 while !stack.is_empty() {
248 match advance_merge(stack.last_mut().expect("stack is not empty")) {
249 DiffStep::Emit(change) => {
250 if let ControlFlow::Break(b) = visitor(change) {
251 return Ok(ControlFlow::Break(b));
252 }
253 }
254 DiffStep::Descend {
255 from_hash,
256 to_hash,
257 name,
258 } => {
259 let from_subtree = match from_hash {
260 Some(hash) => store.get_tree(&hash).await?,
261 None => None,
262 };
263 let to_subtree = match to_hash {
264 Some(hash) => store.get_tree(&hash).await?,
265 None => None,
266 };
267 let prefix = child_path(
268 &stack.last().expect("parent frame remains on stack").prefix,
269 &name,
270 );
271 stack.push(DiffFrame {
272 from: from_subtree,
273 to: to_subtree,
274 prefix,
275 from_index: 0,
276 to_index: 0,
277 });
278 }
279 DiffStep::Done => {
280 stack.pop();
281 }
282 }
283 }
284
285 Ok(ControlFlow::Continue(()))
286}
287
288fn child_path(prefix: &str, name: &str) -> String {
289 if prefix.is_empty() {
290 name.to_owned()
291 } else {
292 let mut path = String::with_capacity(prefix.len() + 1 + name.len());
293 path.push_str(prefix);
294 path.push('/');
295 path.push_str(name);
296 path
297 }
298}