|
| 1 | +use crate::bstr::{BStr, BString, ByteVec}; |
| 2 | +use crate::ext::ObjectIdExt; |
| 3 | +use crate::{Id, Repository, Tree}; |
| 4 | +use git_object::TreeRefIter; |
| 5 | +use git_odb::FindExt; |
| 6 | + |
| 7 | +/// The error return by methods on the [diff platform][Platform]. |
| 8 | +#[derive(Debug, thiserror::Error)] |
| 9 | +#[allow(missing_docs)] |
| 10 | +pub enum Error { |
| 11 | + #[error(transparent)] |
| 12 | + Diff(#[from] git_diff::tree::changes::Error), |
| 13 | + #[error("The user-provided callback failed")] |
| 14 | + ForEach(#[source] Box<dyn std::error::Error + Send + Sync + 'static>), |
| 15 | +} |
| 16 | + |
| 17 | +/// Returned by the `for_each` function to control flow. |
| 18 | +#[derive(Clone, Copy, PartialOrd, PartialEq, Ord, Eq, Hash)] |
| 19 | +pub enum Action { |
| 20 | + /// Continue the traversal of changes. |
| 21 | + Continue, |
| 22 | + /// Stop the traversal of changes and stop calling this function. |
| 23 | + Cancel, |
| 24 | +} |
| 25 | + |
| 26 | +impl Default for Action { |
| 27 | + fn default() -> Self { |
| 28 | + Action::Continue |
| 29 | + } |
| 30 | +} |
| 31 | + |
| 32 | +/// Represents any possible change in order to turn one tree into another. |
| 33 | +#[derive(Debug, Clone, Copy)] |
| 34 | +pub struct Change<'a, 'repo, 'other_repo> { |
| 35 | + /// The location of the file or directory described by `event`, if tracking was enabled. |
| 36 | + /// |
| 37 | + /// Otherwise this value is always an empty path. |
| 38 | + pub location: &'a BStr, |
| 39 | + /// The diff event itself to provide information about what would need to change. |
| 40 | + pub event: Event<'repo, 'other_repo>, |
| 41 | +} |
| 42 | + |
| 43 | +/// An event emitted when finding differences between two trees. |
| 44 | +#[derive(Debug, Clone, Copy)] |
| 45 | +pub enum Event<'repo, 'other_repo> { |
| 46 | + /// An entry was added, like the addition of a file or directory. |
| 47 | + Addition { |
| 48 | + /// The mode of the added entry. |
| 49 | + entry_mode: git_object::tree::EntryMode, |
| 50 | + /// The object id of the added entry. |
| 51 | + id: Id<'other_repo>, |
| 52 | + }, |
| 53 | + /// An entry was deleted, like the deletion of a file or directory. |
| 54 | + Deletion { |
| 55 | + /// The mode of the deleted entry. |
| 56 | + entry_mode: git_object::tree::EntryMode, |
| 57 | + /// The object id of the deleted entry. |
| 58 | + id: Id<'repo>, |
| 59 | + }, |
| 60 | + /// An entry was modified, e.g. changing the contents of a file adjusts its object id and turning |
| 61 | + /// a file into a symbolic link adjusts its mode. |
| 62 | + Modification { |
| 63 | + /// The mode of the entry before the modification. |
| 64 | + previous_entry_mode: git_object::tree::EntryMode, |
| 65 | + /// The object id of the entry before the modification. |
| 66 | + previous_id: Id<'repo>, |
| 67 | + |
| 68 | + /// The mode of the entry after the modification. |
| 69 | + entry_mode: git_object::tree::EntryMode, |
| 70 | + /// The object id after the modification. |
| 71 | + id: Id<'other_repo>, |
| 72 | + }, |
| 73 | +} |
| 74 | + |
| 75 | +/// Diffing |
| 76 | +impl<'repo> Tree<'repo> { |
| 77 | + /// Return a platform to see the changes needed to create other trees, for instance. |
| 78 | + pub fn changes<'a>(&'a self) -> Platform<'a, 'repo> { |
| 79 | + Platform { |
| 80 | + state: Default::default(), |
| 81 | + lhs: self, |
| 82 | + tracking: None, |
| 83 | + } |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +/// The diffing platform returned by [`Tree::changes()`]. |
| 88 | +#[derive(Clone)] |
| 89 | +pub struct Platform<'a, 'repo> { |
| 90 | + state: git_diff::tree::State, |
| 91 | + lhs: &'a Tree<'repo>, |
| 92 | + tracking: Option<Tracking>, |
| 93 | +} |
| 94 | + |
| 95 | +#[derive(Clone, Copy)] |
| 96 | +enum Tracking { |
| 97 | + FileName, |
| 98 | +} |
| 99 | + |
| 100 | +/// Configuration |
| 101 | +impl<'a, 'repo> Platform<'a, 'repo> { |
| 102 | + /// Keep track of file-names, which makes the [`location`][Change::location] field usable with the filename of the changed item. |
| 103 | + pub fn track_filename(&mut self) -> &mut Self { |
| 104 | + self.tracking = Some(Tracking::FileName); |
| 105 | + self |
| 106 | + } |
| 107 | +} |
| 108 | + |
| 109 | +/// Add the item to compare to. |
| 110 | +impl<'a, 'repo> Platform<'a, 'repo> { |
| 111 | + /// Call `for_each` repeatedly with all changes that are needed to convert the source of the diff to the tree to `other`. |
| 112 | + pub fn for_each_to_obtain_tree<'other_repo, E>( |
| 113 | + &mut self, |
| 114 | + other: &Tree<'other_repo>, |
| 115 | + for_each: impl FnMut(Change<'_, 'repo, 'other_repo>) -> Result<Action, E>, |
| 116 | + ) -> Result<(), Error> |
| 117 | + where |
| 118 | + E: std::error::Error + Sync + Send + 'static, |
| 119 | + { |
| 120 | + let repo = self.lhs.repo; |
| 121 | + let mut delegate = Delegate { |
| 122 | + repo: self.lhs.repo, |
| 123 | + other_repo: other.repo, |
| 124 | + tracking: self.tracking, |
| 125 | + location: BString::default(), |
| 126 | + visit: for_each, |
| 127 | + err: None, |
| 128 | + }; |
| 129 | + git_diff::tree::Changes::from(TreeRefIter::from_bytes(&self.lhs.data)).needed_to_obtain( |
| 130 | + TreeRefIter::from_bytes(&other.data), |
| 131 | + &mut self.state, |
| 132 | + |oid, buf| repo.objects.find_tree_iter(oid, buf), |
| 133 | + &mut delegate, |
| 134 | + )?; |
| 135 | + match delegate.err { |
| 136 | + Some(err) => Err(Error::ForEach(Box::new(err))), |
| 137 | + None => Ok(()), |
| 138 | + } |
| 139 | + } |
| 140 | +} |
| 141 | + |
| 142 | +struct Delegate<'repo, 'other_repo, VisitFn, E> { |
| 143 | + repo: &'repo Repository, |
| 144 | + other_repo: &'other_repo Repository, |
| 145 | + tracking: Option<Tracking>, |
| 146 | + location: BString, |
| 147 | + visit: VisitFn, |
| 148 | + err: Option<E>, |
| 149 | +} |
| 150 | + |
| 151 | +impl<'repo, 'other_repo, VisitFn, E> git_diff::tree::Visit for Delegate<'repo, 'other_repo, VisitFn, E> |
| 152 | +where |
| 153 | + VisitFn: for<'delegate> FnMut(Change<'delegate, 'repo, 'other_repo>) -> Result<Action, E>, |
| 154 | + E: std::error::Error + Sync + Send + 'static, |
| 155 | +{ |
| 156 | + fn pop_front_tracked_path_and_set_current(&mut self) {} |
| 157 | + |
| 158 | + fn push_back_tracked_path_component(&mut self, _component: &BStr) { |
| 159 | + {} |
| 160 | + } |
| 161 | + |
| 162 | + fn push_path_component(&mut self, component: &BStr) { |
| 163 | + match self.tracking { |
| 164 | + Some(Tracking::FileName) => { |
| 165 | + self.location.clear(); |
| 166 | + self.location.push_str(component); |
| 167 | + } |
| 168 | + None => {} |
| 169 | + } |
| 170 | + } |
| 171 | + |
| 172 | + fn pop_path_component(&mut self) {} |
| 173 | + |
| 174 | + fn visit(&mut self, change: git_diff::tree::visit::Change) -> git_diff::tree::visit::Action { |
| 175 | + use git_diff::tree::visit::Change::*; |
| 176 | + let event = match change { |
| 177 | + Addition { entry_mode, oid } => Event::Addition { |
| 178 | + entry_mode, |
| 179 | + id: oid.attach(self.other_repo), |
| 180 | + }, |
| 181 | + Deletion { entry_mode, oid } => Event::Deletion { |
| 182 | + entry_mode, |
| 183 | + id: oid.attach(self.repo), |
| 184 | + }, |
| 185 | + Modification { |
| 186 | + previous_entry_mode, |
| 187 | + previous_oid, |
| 188 | + entry_mode, |
| 189 | + oid, |
| 190 | + } => Event::Modification { |
| 191 | + previous_entry_mode, |
| 192 | + entry_mode, |
| 193 | + previous_id: previous_oid.attach(self.repo), |
| 194 | + id: oid.attach(self.other_repo), |
| 195 | + }, |
| 196 | + }; |
| 197 | + match (self.visit)(Change { |
| 198 | + event, |
| 199 | + location: self.location.as_ref(), |
| 200 | + }) { |
| 201 | + Ok(Action::Cancel) => git_diff::tree::visit::Action::Cancel, |
| 202 | + Ok(Action::Continue) => git_diff::tree::visit::Action::Continue, |
| 203 | + Err(err) => { |
| 204 | + self.err = Some(err); |
| 205 | + git_diff::tree::visit::Action::Cancel |
| 206 | + } |
| 207 | + } |
| 208 | + } |
| 209 | +} |
0 commit comments