Skip to content

Commit 588155f

Browse files
committed
feat: add first 'debug' version of gix diff file
1 parent d0ef276 commit 588155f

File tree

3 files changed

+253
-1
lines changed

3 files changed

+253
-1
lines changed

gitoxide-core/src/repository/diff.rs

Lines changed: 226 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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,225 @@ fn typed_location(mut location: BString, mode: EntryMode) -> BString {
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+
match change {
249+
recorder::Change::Addition { oid, .. } => {
250+
// Setting `OldOrSource` to `ObjectId::empty_blob` makes `diff` see an addition.
251+
resource_cache.set_resource(
252+
ObjectId::empty_blob(gix::hash::Kind::Sha1),
253+
gix::object::tree::EntryKind::Blob,
254+
path.as_slice().into(),
255+
gix::diff::blob::ResourceKind::OldOrSource,
256+
&repo.objects,
257+
)?;
258+
resource_cache.set_resource(
259+
oid,
260+
gix::object::tree::EntryKind::Blob,
261+
path.as_slice().into(),
262+
gix::diff::blob::ResourceKind::NewOrDestination,
263+
&repo.objects,
264+
)?;
265+
}
266+
recorder::Change::Deletion { oid: previous_oid, .. } => {
267+
resource_cache.set_resource(
268+
previous_oid,
269+
gix::object::tree::EntryKind::Blob,
270+
path.as_slice().into(),
271+
gix::diff::blob::ResourceKind::OldOrSource,
272+
&repo.objects,
273+
)?;
274+
// Setting `NewOrDestination` to `ObjectId::empty_blob` makes `diff` see a deletion.
275+
resource_cache.set_resource(
276+
ObjectId::empty_blob(gix::hash::Kind::Sha1),
277+
gix::object::tree::EntryKind::Blob,
278+
path.as_slice().into(),
279+
gix::diff::blob::ResourceKind::NewOrDestination,
280+
&repo.objects,
281+
)?;
282+
}
283+
recorder::Change::Modification {
284+
previous_oid,
285+
oid,
286+
path,
287+
..
288+
} => {
289+
resource_cache.set_resource(
290+
previous_oid,
291+
gix::object::tree::EntryKind::Blob,
292+
path.as_slice().into(),
293+
gix::diff::blob::ResourceKind::OldOrSource,
294+
&repo.objects,
295+
)?;
296+
resource_cache.set_resource(
297+
oid,
298+
gix::object::tree::EntryKind::Blob,
299+
path.as_slice().into(),
300+
gix::diff::blob::ResourceKind::NewOrDestination,
301+
&repo.objects,
302+
)?;
303+
}
304+
}
305+
306+
let outcome = resource_cache.prepare_diff()?;
307+
308+
let old_data = String::from_utf8_lossy(outcome.old.data.as_slice().unwrap_or_default());
309+
let new_data = String::from_utf8_lossy(outcome.new.data.as_slice().unwrap_or_default());
310+
311+
let input =
312+
gix::diff::blob::intern::InternedInput::new(tokens_for_diffing(&old_data), tokens_for_diffing(&new_data));
313+
314+
let unified_diff_builder = UnifiedDiffBuilder::new(&input);
315+
316+
use gix::diff::blob::platform::prepare_diff::Operation;
317+
318+
let algorithm = match outcome.operation {
319+
Operation::InternalDiff { algorithm } => algorithm,
320+
Operation::ExternalCommand { .. } => {
321+
// `unreachable!` is also used in [`Platform::lines()`](gix::object::blob::diff::Platform::lines()).
322+
unreachable!("We disabled that")
323+
}
324+
Operation::SourceOrDestinationIsBinary => {
325+
anyhow::bail!("Source or destination is binary and we can't diff that")
326+
}
327+
};
328+
329+
let unified_diff = gix::diff::blob::diff(algorithm, &input, unified_diff_builder);
330+
331+
out.write_all(unified_diff.as_bytes())?;
332+
333+
Ok(())
334+
}
335+
336+
pub(crate) fn tokens_for_diffing(data: &str) -> impl TokenSource<Token = &str> {
337+
gix::diff::blob::sources::lines(data)
338+
}

src/plumbing/main.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,21 @@ pub fn main() -> Result<()> {
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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,18 @@ pub mod diff {
514514
#[clap(value_parser = crate::shared::AsBString)]
515515
new_treeish: BString,
516516
},
517+
/// Diff two versions of a file.
518+
File {
519+
/// A rev-spec representing the 'before' or old tree.
520+
#[clap(value_parser = crate::shared::AsBString)]
521+
old_treeish: BString,
522+
/// A rev-spec representing the 'after' or new tree.
523+
#[clap(value_parser = crate::shared::AsBString)]
524+
new_treeish: BString,
525+
/// The path to the file to run diff for.
526+
#[clap(value_parser = crate::shared::AsBString)]
527+
path: BString,
528+
},
517529
}
518530
}
519531

0 commit comments

Comments
 (0)