History

refactor

Sebastian Thiel committed 5 years ago

Commit c0318cf1 parent aaba9dc7 files · +51 −51Browse files

Showing the changes to git-diff/src/tree (4 of 7 files). Show all

Unified · Split

4 files
  1. git-diff/src/tree/changes.rs +364 −0
  2. git-diff/src/tree/mod.rs +49 −0
  3. git-diff/src/tree/recorder.rs +121 −0
  4. git-diff/src/tree/visit.rs +83 −0
git-diff/src/tree/changes.rs+364−0
@@ -0,0 +1,364 @@
1use crate::{
2 tree,
3 tree::{changes::Error::Cancelled, visit::Change, TreeInfo, TreeInfoPair},
4};
5use git_hash::{oid, ObjectId};
6use git_object::immutable;
7use quick_error::quick_error;
8use std::{borrow::BorrowMut, collections::VecDeque};
9
10quick_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
27impl<'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
114fn 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
143fn 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}
171fn 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
211fn 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
251fn 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
360type IteratorType<I> = std::mem::ManuallyDrop<std::iter::Peekable<I>>;
361
362fn peekable<I: Iterator>(iter: I) -> IteratorType<I> {
363 std::mem::ManuallyDrop::new(iter.peekable())
364}
git-diff/src/tree/mod.rs+49−0
@@ -0,0 +1,49 @@
1use git_hash::ObjectId;
2use git_object::immutable;
3use std::collections::VecDeque;
4
5/// The state required to visit [Changes] to be instantiated with `State::default()`.
6#[derive(Default, Clone)]
7pub struct State<PathId: Default + Clone> {
8 buf1: Vec<u8>,
9 buf2: Vec<u8>,
10 trees: VecDeque<TreeInfoPair<PathId>>,
11}
12
13type TreeInfoPair<PathId> = (Option<TreeInfo<PathId>>, Option<TreeInfo<PathId>>);
14
15#[derive(Clone)]
16pub(crate) struct TreeInfo<PathId: Clone> {
17 pub tree_id: ObjectId,
18 pub parent_path_id: PathId,
19}
20
21impl<P: Clone + Default> State<P> {
22 fn clear(&mut self) {
23 self.trees.clear();
24 self.buf1.clear();
25 self.buf2.clear();
26 }
27}
28
29/// An iterator over changes of a tree, instantiated using `Changes::from(…)`.
30pub struct Changes<'a>(Option<immutable::TreeIter<'a>>);
31
32impl<'a, T> From<T> for Changes<'a>
33where
34 T: Into<Option<immutable::TreeIter<'a>>>,
35{
36 fn from(v: T) -> Self {
37 Changes(v.into())
38 }
39}
40
41mod changes;
42
43///
44pub mod visit;
45pub use visit::Visit;
46
47/// Houses a [`visit::Visit`] to keep track of all seen changes. Useful for debugging primarily.
48pub mod recorder;
49pub use recorder::Recorder;
git-diff/src/tree/recorder.rs+121−0
@@ -0,0 +1,121 @@
1use crate::tree::visit;
2use git_hash::ObjectId;
3use git_object::{
4 bstr::{BStr, BString, ByteSlice, ByteVec},
5 tree,
6};
7use std::{collections::BTreeMap, ops::Deref, path::PathBuf};
8
9/// A Change as observed by a call to [`visit::Visit::visit`], enhanced with the path affected by the change.
10/// Its similar to [visit::Change] but adds a path.
11#[derive(Clone, Debug, PartialEq, Eq)]
12#[allow(missing_docs)]
13pub enum Change {
14 Addition {
15 entry_mode: tree::EntryMode,
16 oid: ObjectId,
17 path: PathBuf,
18 },
19 Deletion {
20 entry_mode: tree::EntryMode,
21 oid: ObjectId,
22 path: PathBuf,
23 },
24 Modification {
25 previous_entry_mode: tree::EntryMode,
26 previous_oid: ObjectId,
27
28 entry_mode: tree::EntryMode,
29 oid: ObjectId,
30
31 path: PathBuf,
32 },
33}
34
35/// A [visit::Visit] implementation to record every observed change and keep track of the changed paths.
36#[derive(Clone, Debug, Default)]
37pub struct Recorder {
38 path_count: usize,
39 path_map: BTreeMap<usize, BString>,
40 path: BString,
41 /// The observed changes.
42 pub records: Vec<Change>,
43}
44
45impl Recorder {
46 fn pop_element(&mut self) {
47 if let Some(pos) = self.path.rfind_byte(b'/') {
48 self.path.resize(pos, 0);
49 } else {
50 self.path.clear();
51 }
52 }
53
54 fn push_element(&mut self, name: &BStr) {
55 if !self.path.is_empty() {
56 self.path.push(b'/');
57 }
58 self.path.push_str(name);
59 }
60
61 fn path_clone(&self) -> BString {
62 self.path.clone()
63 }
64
65 fn path_buf(&self) -> PathBuf {
66 self.path.deref().to_owned().into_path_buf_lossy()
67 }
68}
69
70impl visit::Visit for Recorder {
71 type PathId = usize;
72
73 fn set_current_path(&mut self, path: Self::PathId) {
74 self.path = self.path_map.remove(&path).expect("every parent is set only once");
75 }
76
77 fn push_tracked_path_component(&mut self, component: &BStr) -> Self::PathId {
78 self.push_element(component);
79 self.path_map.insert(self.path_count, self.path_clone());
80 let res = self.path_count;
81 self.path_count += 1;
82 res
83 }
84
85 fn push_path_component(&mut self, component: &BStr) {
86 self.push_element(component);
87 }
88
89 fn pop_path_component(&mut self) {
90 self.pop_element();
91 }
92
93 fn visit(&mut self, change: visit::Change) -> visit::Action {
94 use visit::Change::*;
95 self.records.push(match change {
96 Deletion { entry_mode, oid } => Change::Deletion {
97 entry_mode,
98 oid,
99 path: self.path_buf(),
100 },
101 Addition { entry_mode, oid } => Change::Addition {
102 entry_mode,
103 oid,
104 path: self.path_buf(),
105 },
106 Modification {
107 previous_entry_mode,
108 previous_oid,
109 entry_mode,
110 oid,
111 } => Change::Modification {
112 previous_entry_mode,
113 previous_oid,
114 entry_mode,
115 oid,
116 path: self.path_buf(),
117 },
118 });
119 visit::Action::Continue
120 }
121}
git-diff/src/tree/visit.rs+83−0
@@ -0,0 +1,83 @@
1use git_hash::ObjectId;
2use git_object::{bstr::BStr, tree};
3
4/// Represents any possible change in order to turn one tree into another.
5pub enum Change {
6 /// An entry was added, like the addition of a file or directory.
7 Addition {
8 /// The mode of the added entry.
9 entry_mode: tree::EntryMode,
10 /// The object id of the added entry.
11 oid: ObjectId,
12 },
13 /// An entry was deleted, like the deletion of a file or directory.
14 Deletion {
15 /// The mode of the deleted entry.
16 entry_mode: tree::EntryMode,
17 /// The object id of the deleted entry.
18 oid: ObjectId,
19 },
20 /// An entry was modified, e.g. changing the contents of a file adjusts its object id and turning
21 /// a file into a symbolic link adjusts its mode.
22 Modification {
23 /// The mode of the entry before the modification.
24 previous_entry_mode: tree::EntryMode,
25 /// The object id of the entry before the modification.
26 previous_oid: ObjectId,
27
28 /// The mode of the entry after the modification.
29 entry_mode: tree::EntryMode,
30 /// The object id of the after before the modification.
31 oid: ObjectId,
32 },
33}
34
35/// What to do after a [Change] was [recorded][Visit::visit()].
36#[derive(Clone, Copy, PartialOrd, PartialEq, Ord, Eq, Hash)]
37pub enum Action {
38 /// Continue the traversal of changes.
39 Continue,
40 /// Stop the traversal of changes, making this te last call to [record(…)][Visit::visit()].
41 Cancel,
42}
43
44impl Action {
45 /// Returns true if this action means to stop the traversal.
46 pub fn cancelled(&self) -> bool {
47 matches!(self, Action::Cancel)
48 }
49}
50
51/// A trait to allow responding to a traversal designed to figure out the [changes][Change]
52/// to turn tree A into tree B.
53pub trait Visit {
54 /// A type capable of uniquely identifying paths in a tree.
55 type PathId: Clone + Default;
56
57 /// Sets the path associated with the given `id` so future calls to push and pop components affect it instead.
58 fn set_current_path(&mut self, id: Self::PathId);
59 /// Append a `component` to the end of a path, which may be empty, and associate it with the returned path id.
60 fn push_tracked_path_component(&mut self, component: &BStr) -> Self::PathId;
61 /// Append a `component` to the end of a path, which may be empty.
62 fn push_path_component(&mut self, component: &BStr);
63 /// Removes the last component from the path, which may leave it empty.
64 fn pop_path_component(&mut self);
65 /// Record a `change` and return an instruction whether to continue or not.
66 ///
67 /// The implementation may use the current path to lean where in the tree the change is located.
68 fn visit(&mut self, change: Change) -> Action;
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn size_of_change() {
77 assert_eq!(
78 std::mem::size_of::<Change>(),
79 46,
80 "this type shouldn't grow without us knowing"
81 )
82 }
83}