| @@ -1,7 +1,10 @@ |
| 1 | use gix::bstr::{BString, ByteSlice}; | 1 | use gix::bstr::{BStr, BString, ByteSlice}; |
| | 2 | use gix::diff::blob::intern::TokenSource; |
| | 3 | use gix::diff::blob::UnifiedDiffBuilder; |
| 2 | use gix::objs::tree::EntryMode; | 4 | use gix::objs::tree::EntryMode; |
| 3 | use gix::odb::store::RefreshMode; | 5 | use gix::odb::store::RefreshMode; |
| 4 | use gix::prelude::ObjectIdExt; | 6 | use gix::prelude::ObjectIdExt; |
| | 7 | use gix::ObjectId; |
| 5 | | 8 | |
| 6 | pub fn tree( | 9 | pub fn tree( |
| 7 | mut repo: gix::Repository, | 10 | mut repo: gix::Repository, |
| @@ -111,3 +114,194 @@ fn typed_location(mut location: BString, mode: EntryMode) -> BString { · 103 unmodified lines |
| 111 | } | 114 | } |
| 112 | location | 115 | location |
| 113 | } | 116 | } |
| | 117 | |
| | 118 | pub fn file( |
| | 119 | mut repo: gix::Repository, |
| | 120 | out: &mut dyn std::io::Write, |
| | 121 | old_treeish: BString, |
| | 122 | new_treeish: BString, |
| | 123 | path: BString, |
| | 124 | ) -> Result<(), anyhow::Error> { |
| | 125 | repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&**repo.index_or_empty()?)); |
| | 126 | repo.objects.refresh = RefreshMode::Never; |
| | 127 | |
| | 128 | let old_tree_id = repo.rev_parse_single(old_treeish.as_bstr())?; |
| | 129 | let new_tree_id = repo.rev_parse_single(new_treeish.as_bstr())?; |
| | 130 | |
| | 131 | let old_tree = old_tree_id.object()?.peel_to_tree()?; |
| | 132 | let new_tree = new_tree_id.object()?.peel_to_tree()?; |
| | 133 | |
| | 134 | let mut old_tree_buf = Vec::new(); |
| | 135 | let mut new_tree_buf = Vec::new(); |
| | 136 | |
| | 137 | use gix::diff::object::FindExt; |
| | 138 | |
| | 139 | let old_tree_iter = repo.objects.find_tree_iter(&old_tree.id(), &mut old_tree_buf)?; |
| | 140 | let new_tree_iter = repo.objects.find_tree_iter(&new_tree.id(), &mut new_tree_buf)?; |
| | 141 | |
| | 142 | use gix::diff::tree::{ |
| | 143 | recorder::{self, Location}, |
| | 144 | Recorder, |
| | 145 | }; |
| | 146 | |
| | 147 | struct FindChangeToPath { |
| | 148 | inner: Recorder, |
| | 149 | interesting_path: BString, |
| | 150 | change: Option<recorder::Change>, |
| | 151 | } |
| | 152 | |
| | 153 | impl FindChangeToPath { |
| | 154 | fn new(interesting_path: &BStr) -> Self { |
| | 155 | let inner = Recorder::default().track_location(Some(Location::Path)); |
| | 156 | |
| | 157 | FindChangeToPath { |
| | 158 | inner, |
| | 159 | interesting_path: interesting_path.into(), |
| | 160 | change: None, |
| | 161 | } |
| | 162 | } |
| | 163 | } |
| | 164 | |
| | 165 | use gix::diff::tree::{visit, Visit}; |
| | 166 | |
| | 167 | impl Visit for FindChangeToPath { |
| | 168 | fn pop_front_tracked_path_and_set_current(&mut self) { |
| | 169 | self.inner.pop_front_tracked_path_and_set_current(); |
| | 170 | } |
| | 171 | |
| | 172 | fn push_back_tracked_path_component(&mut self, component: &BStr) { |
| | 173 | self.inner.push_back_tracked_path_component(component); |
| | 174 | } |
| | 175 | |
| | 176 | fn push_path_component(&mut self, component: &BStr) { |
| | 177 | self.inner.push_path_component(component); |
| | 178 | } |
| | 179 | |
| | 180 | fn pop_path_component(&mut self) { |
| | 181 | self.inner.pop_path_component(); |
| | 182 | } |
| | 183 | |
| | 184 | fn visit(&mut self, change: visit::Change) -> visit::Action { |
| | 185 | if self.inner.path() == self.interesting_path { |
| | 186 | self.change = Some(match change { |
| | 187 | visit::Change::Deletion { |
| | 188 | entry_mode, |
| | 189 | oid, |
| | 190 | relation, |
| | 191 | } => recorder::Change::Deletion { |
| | 192 | entry_mode, |
| | 193 | oid, |
| | 194 | path: self.inner.path_clone(), |
| | 195 | relation, |
| | 196 | }, |
| | 197 | visit::Change::Addition { |
| | 198 | entry_mode, |
| | 199 | oid, |
| | 200 | relation, |
| | 201 | } => recorder::Change::Addition { |
| | 202 | entry_mode, |
| | 203 | oid, |
| | 204 | path: self.inner.path_clone(), |
| | 205 | relation, |
| | 206 | }, |
| | 207 | visit::Change::Modification { |
| | 208 | previous_entry_mode, |
| | 209 | previous_oid, |
| | 210 | entry_mode, |
| | 211 | oid, |
| | 212 | } => recorder::Change::Modification { |
| | 213 | previous_entry_mode, |
| | 214 | previous_oid, |
| | 215 | entry_mode, |
| | 216 | oid, |
| | 217 | path: self.inner.path_clone(), |
| | 218 | }, |
| | 219 | }); |
| | 220 | |
| | 221 | visit::Action::Cancel |
| | 222 | } else { |
| | 223 | visit::Action::Continue |
| | 224 | } |
| | 225 | } |
| | 226 | } |
| | 227 | |
| | 228 | let mut recorder = FindChangeToPath::new(path.as_ref()); |
| | 229 | let state = gix::diff::tree::State::default(); |
| | 230 | let result = gix::diff::tree(old_tree_iter, new_tree_iter, state, &repo.objects, &mut recorder); |
| | 231 | |
| | 232 | let change = match result { |
| | 233 | Ok(_) | Err(gix::diff::tree::Error::Cancelled) => recorder.change, |
| | 234 | Err(error) => return Err(error.into()), |
| | 235 | }; |
| | 236 | |
| | 237 | let Some(change) = change else { |
| | 238 | anyhow::bail!( |
| | 239 | "There was no change to {} between {} and {}", |
| | 240 | &path, |
| | 241 | old_treeish, |
| | 242 | new_treeish |
| | 243 | ) |
| | 244 | }; |
| | 245 | |
| | 246 | let mut resource_cache = repo.diff_resource_cache(gix::diff::blob::pipeline::Mode::ToGit, Default::default())?; |
| | 247 | |
| | 248 | let (previous_oid, oid) = match change { |
| | 249 | recorder::Change::Addition { oid, .. } => { |
| | 250 | // Setting `previous_oid` to `ObjectId::empty_blob` makes `diff` see an addition. |
| | 251 | (ObjectId::empty_blob(gix::hash::Kind::Sha1), oid) |
| | 252 | } |
| | 253 | recorder::Change::Deletion { oid: previous_oid, .. } => { |
| | 254 | // Setting `oid` to `ObjectId::empty_blob` makes `diff` see a deletion. |
| | 255 | (previous_oid, ObjectId::empty_blob(gix::hash::Kind::Sha1)) |
| | 256 | } |
| | 257 | recorder::Change::Modification { previous_oid, oid, .. } => (previous_oid, oid), |
| | 258 | }; |
| | 259 | |
| | 260 | resource_cache.set_resource( |
| | 261 | previous_oid, |
| | 262 | gix::object::tree::EntryKind::Blob, |
| | 263 | path.as_slice().into(), |
| | 264 | gix::diff::blob::ResourceKind::OldOrSource, |
| | 265 | &repo.objects, |
| | 266 | )?; |
| | 267 | resource_cache.set_resource( |
| | 268 | oid, |
| | 269 | gix::object::tree::EntryKind::Blob, |
| | 270 | path.as_slice().into(), |
| | 271 | gix::diff::blob::ResourceKind::NewOrDestination, |
| | 272 | &repo.objects, |
| | 273 | )?; |
| | 274 | |
| | 275 | let outcome = resource_cache.prepare_diff()?; |
| | 276 | |
| | 277 | let old_data = String::from_utf8_lossy(outcome.old.data.as_slice().unwrap_or_default()); |
| | 278 | let new_data = String::from_utf8_lossy(outcome.new.data.as_slice().unwrap_or_default()); |
| | 279 | |
| | 280 | let input = |
| | 281 | gix::diff::blob::intern::InternedInput::new(tokens_for_diffing(&old_data), tokens_for_diffing(&new_data)); |
| | 282 | |
| | 283 | let unified_diff_builder = UnifiedDiffBuilder::new(&input); |
| | 284 | |
| | 285 | use gix::diff::blob::platform::prepare_diff::Operation; |
| | 286 | |
| | 287 | let algorithm = match outcome.operation { |
| | 288 | Operation::InternalDiff { algorithm } => algorithm, |
| | 289 | Operation::ExternalCommand { .. } => { |
| | 290 | // `unreachable!` is also used in [`Platform::lines()`](gix::object::blob::diff::Platform::lines()). |
| | 291 | unreachable!("We disabled that") |
| | 292 | } |
| | 293 | Operation::SourceOrDestinationIsBinary => { |
| | 294 | anyhow::bail!("Source or destination is binary and we can't diff that") |
| | 295 | } |
| | 296 | }; |
| | 297 | |
| | 298 | let unified_diff = gix::diff::blob::diff(algorithm, &input, unified_diff_builder); |
| | 299 | |
| | 300 | out.write_all(unified_diff.as_bytes())?; |
| | 301 | |
| | 302 | Ok(()) |
| | 303 | } |
| | 304 | |
| | 305 | pub(crate) fn tokens_for_diffing(data: &str) -> impl TokenSource<Token = &str> { |
| | 306 | gix::diff::blob::sources::lines(data) |
| | 307 | } |