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, Some(from), to, |change| {
138 changes.push(change);
139 ControlFlow::<()>::Continue(())
140 })?;
141 Ok(changes)
142}
143
144pub fn diff_trees_visit<S, V, B>(
160 store: &S,
161 from: Option<&crate::object::ContentHash>,
162 to: &crate::object::ContentHash,
163 mut visitor: V,
164) -> Result<ControlFlow<B>, anyhow::Error>
165where
166 S: ObjectSource + ?Sized,
167 V: FnMut(FileChange) -> ControlFlow<B>,
168{
169 let from_tree = from.map(|hash| store.require_tree(hash)).transpose()?;
170 if from == Some(to) {
171 return Ok(ControlFlow::Continue(()));
172 }
173 let to_tree = Some(store.require_tree(to)?);
174 let mut stack = vec![DiffFrame {
175 from: from_tree,
176 to: to_tree,
177 prefix: String::new(),
178 from_index: 0,
179 to_index: 0,
180 }];
181
182 while let Some(frame) = stack.last_mut() {
183 match advance_merge(frame) {
184 DiffStep::Emit(change) => {
185 if let ControlFlow::Break(b) = visitor(change) {
186 return Ok(ControlFlow::Break(b));
187 }
188 }
189 DiffStep::Descend {
190 from_hash,
191 to_hash,
192 name,
193 } => {
194 let from_subtree = from_hash
195 .map(|hash| store.require_tree(&hash))
196 .transpose()?;
197 let to_subtree = to_hash.map(|hash| store.require_tree(&hash)).transpose()?;
198 let prefix = child_path(&frame.prefix, &name);
199 stack.push(DiffFrame {
200 from: from_subtree,
201 to: to_subtree,
202 prefix,
203 from_index: 0,
204 to_index: 0,
205 });
206 }
207 DiffStep::Done => {
208 stack.pop();
209 }
210 }
211 }
212
213 Ok(ControlFlow::Continue(()))
214}
215
216#[cfg(feature = "async-source")]
217pub async fn diff_trees_visit_async<S, V, B>(
218 store: &S,
219 from: Option<&crate::object::ContentHash>,
220 to: &crate::object::ContentHash,
221 mut visitor: V,
222) -> Result<ControlFlow<B>, anyhow::Error>
223where
224 S: AsyncObjectSource + Sync + ?Sized,
225 V: FnMut(FileChange) -> ControlFlow<B> + Send,
226 B: Send,
227{
228 let from_tree = match from {
229 Some(hash) => Some(store.require_tree(hash).await?),
230 None => None,
231 };
232 if from == Some(to) {
233 return Ok(ControlFlow::Continue(()));
234 }
235 let to_tree = Some(store.require_tree(to).await?);
236 let mut stack = vec![DiffFrame {
237 from: from_tree,
238 to: to_tree,
239 prefix: String::new(),
240 from_index: 0,
241 to_index: 0,
242 }];
243
244 while let Some(frame) = stack.last_mut() {
245 match advance_merge(frame) {
246 DiffStep::Emit(change) => {
247 if let ControlFlow::Break(b) = visitor(change) {
248 return Ok(ControlFlow::Break(b));
249 }
250 }
251 DiffStep::Descend {
252 from_hash,
253 to_hash,
254 name,
255 } => {
256 let from_subtree = match from_hash {
257 Some(hash) => Some(store.require_tree(&hash).await?),
258 None => None,
259 };
260 let to_subtree = match to_hash {
261 Some(hash) => Some(store.require_tree(&hash).await?),
262 None => None,
263 };
264 let prefix = child_path(&frame.prefix, &name);
265 stack.push(DiffFrame {
266 from: from_subtree,
267 to: to_subtree,
268 prefix,
269 from_index: 0,
270 to_index: 0,
271 });
272 }
273 DiffStep::Done => {
274 stack.pop();
275 }
276 }
277 }
278
279 Ok(ControlFlow::Continue(()))
280}
281
282fn child_path(prefix: &str, name: &str) -> String {
283 if prefix.is_empty() {
284 name.to_owned()
285 } else {
286 let mut path = String::with_capacity(prefix.len() + 1 + name.len());
287 path.push_str(prefix);
288 path.push('/');
289 path.push_str(name);
290 path
291 }
292}