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 @@
1+use crate::{
2+ tree,
3+ tree::{changes::Error::Cancelled, visit::Change, TreeInfo, TreeInfoPair},
4+};
5+use git_hash::{oid, ObjectId};
6+use git_object::immutable;
7+use quick_error::quick_error;
8+use std::{borrow::BorrowMut, collections::VecDeque};
9+
10+quick_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+
27+impl<'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+
114+fn 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+
143+fn 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+}
171+fn 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+
211+fn 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+
251+fn 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+
360+type IteratorType<I> = std::mem::ManuallyDrop<std::iter::Peekable<I>>;
361+
362+fn 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 @@
1+use git_hash::ObjectId;
2+use git_object::immutable;
3+use std::collections::VecDeque;
4+
5+/// The state required to visit [Changes] to be instantiated with `State::default()`.
6+#[derive(Default, Clone)]
7+pub struct State<PathId: Default + Clone> {
8+ buf1: Vec<u8>,
9+ buf2: Vec<u8>,
10+ trees: VecDeque<TreeInfoPair<PathId>>,
11+}
12+
13+type TreeInfoPair<PathId> = (Option<TreeInfo<PathId>>, Option<TreeInfo<PathId>>);
14+
15+#[derive(Clone)]
16+pub(crate) struct TreeInfo<PathId: Clone> {
17+ pub tree_id: ObjectId,
18+ pub parent_path_id: PathId,
19+}
20+
21+impl<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(…)`.
30+pub struct Changes<'a>(Option<immutable::TreeIter<'a>>);
31+
32+impl<'a, T> From<T> for Changes<'a>
33+where
34+ T: Into<Option<immutable::TreeIter<'a>>>,
35+{
36+ fn from(v: T) -> Self {
37+ Changes(v.into())
38+ }
39+}
40+
41+mod changes;
42+
43+///
44+pub mod visit;
45+pub use visit::Visit;
46+
47+/// Houses a [`visit::Visit`] to keep track of all seen changes. Useful for debugging primarily.
48+pub mod recorder;
49+pub use recorder::Recorder;
git-diff/src/tree/recorder.rs+121−0
@@ -0,0 +1,121 @@
1+use crate::tree::visit;
2+use git_hash::ObjectId;
3+use git_object::{
4+ bstr::{BStr, BString, ByteSlice, ByteVec},
5+ tree,
6+};
7+use 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)]
13+pub 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)]
37+pub 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+
45+impl 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+
70+impl 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 @@
1+use git_hash::ObjectId;
2+use git_object::{bstr::BStr, tree};
3+
4+/// Represents any possible change in order to turn one tree into another.
5+pub 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)]
37+pub 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+
44+impl 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.
53+pub 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)]
72+mod 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+}