History

feat: add first 'debug' version of `gix diff file`

Christoph Rüßler committed 1 year ago

Commit dbb15321 parent 77dbd4b3 files · +222 −1Browse files

Unified · Split

3 files
  1. gitoxide-core/src/repository/diff.rs +195 −1
  2. src/plumbing/main.rs +15 −0
  3. src/plumbing/options/mod.rs +12 −0
gitoxide-core/src/repository/diff.rs+195−1
@@ -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;
24 use gix::objs::tree::EntryMode;
35 use gix::odb::store::RefreshMode;
46 use gix::prelude::ObjectIdExt;
7+use gix::ObjectId;
58
69 pub fn tree(
710 mut repo: gix::Repository,
@@ -111,3 +114,194 @@ fn typed_location(mut location: BString, mode: EntryMode) -> BString { · 103 unmodified lines
111114 }
112115 location
113116 }
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+}
src/plumbing/main.rs+15−0
@@ -277,6 +277,21 @@ pub fn main() -> Result<()> { · 276 unmodified lines
277277 core::repository::diff::tree(repository(Mode::Lenient)?, out, old_treeish, new_treeish)
278278 },
279279 ),
280+ crate::plumbing::options::diff::SubCommands::File {
281+ old_treeish,
282+ new_treeish,
283+ path,
284+ } => prepare_and_run(
285+ "diff-file",
286+ trace,
287+ verbose,
288+ progress,
289+ progress_keep_open,
290+ None,
291+ move |_progress, out, _err| {
292+ core::repository::diff::file(repository(Mode::Lenient)?, out, old_treeish, new_treeish, path)
293+ },
294+ ),
280295 },
281296 Subcommands::Log(crate::plumbing::options::log::Platform { pathspec }) => prepare_and_run(
282297 "log",
src/plumbing/options/mod.rs+12−0
@@ -517,6 +517,18 @@ pub mod diff { · 516 unmodified lines
517517 #[clap(value_parser = crate::shared::AsBString)]
518518 new_treeish: BString,
519519 },
520+ /// Diff two versions of a file.
521+ File {
522+ /// A rev-spec representing the 'before' or old tree.
523+ #[clap(value_parser = crate::shared::AsBString)]
524+ old_treeish: BString,
525+ /// A rev-spec representing the 'after' or new tree.
526+ #[clap(value_parser = crate::shared::AsBString)]
527+ new_treeish: BString,
528+ /// The path to the file to run diff for.
529+ #[clap(value_parser = crate::shared::AsBString)]
530+ path: BString,
531+ },
520532 }
521533 }
522534

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t