Skip to content

Commit f7e7c4e

Browse files
committed
feat: add Repository::diff_tree_to_tree() for greater similarity to git2
1 parent 743695f commit f7e7c4e

File tree

10 files changed

+503
-58
lines changed

10 files changed

+503
-58
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gitoxide-core/src/hours/core.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,9 @@ pub fn spawn_tree_delta_threads<'scope>(
125125
None => continue,
126126
};
127127
from.changes()?
128-
.track_filename()
129-
.track_rewrites(None)
128+
.options(|opts| {
129+
opts.track_filename().track_rewrites(None);
130+
})
130131
.for_each_to_obtain_tree(&to, |change| {
131132
use gix::object::tree::diff::Change::*;
132133
changes.fetch_add(1, Ordering::Relaxed);

gitoxide-core/src/query/engine/update.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,9 @@ pub fn update(
207207
rewrite_cache.clear_resource_cache_keep_allocation();
208208
diff_cache.clear_resource_cache_keep_allocation();
209209
from.changes()?
210-
.track_path()
211-
.track_rewrites(Some(rewrites))
210+
.options(|opts| {
211+
opts.track_path().track_rewrites(Some(rewrites));
212+
})
212213
.for_each_to_obtain_tree_with_cache(&to, &mut rewrite_cache, |change| {
213214
use gix::object::tree::diff::Change::*;
214215
change_counter.fetch_add(1, Ordering::SeqCst);

gix/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ walkdir = "2.3.2"
408408
serial_test = { version = "3.1.0", default-features = false }
409409
async-std = { version = "1.12.0", features = ["attributes"] }
410410
termtree = "0.5.1"
411+
insta = "1.40.0"
411412

412413
[package.metadata.docs.rs]
413414
features = [

gix/src/diff.rs

+104
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,109 @@
1+
use gix_diff::tree::recorder::Location;
12
pub use gix_diff::*;
23

4+
///
5+
pub mod options {
6+
///
7+
pub mod init {
8+
/// The error returned when instantiating [diff options](crate::diff::Options).
9+
#[derive(Debug, thiserror::Error)]
10+
#[allow(missing_docs)]
11+
pub enum Error {
12+
#[cfg(feature = "blob-diff")]
13+
#[error(transparent)]
14+
RewritesConfiguration(#[from] crate::diff::new_rewrites::Error),
15+
}
16+
}
17+
}
18+
19+
/// General diff-related options for configuring rename-tracking and blob diffs.
20+
#[derive(Debug, Copy, Clone)]
21+
pub struct Options {
22+
location: Option<Location>,
23+
#[cfg(feature = "blob-diff")]
24+
rewrites: Option<gix_diff::Rewrites>,
25+
}
26+
27+
impl Default for Options {
28+
fn default() -> Self {
29+
Options {
30+
location: Some(Location::Path),
31+
#[cfg(feature = "blob-diff")]
32+
rewrites: None,
33+
}
34+
}
35+
}
36+
37+
#[cfg(feature = "blob-diff")]
38+
impl From<Options> for gix_diff::tree_with_rewrites::Options {
39+
fn from(opts: Options) -> Self {
40+
gix_diff::tree_with_rewrites::Options {
41+
location: opts.location,
42+
#[cfg(feature = "blob-diff")]
43+
rewrites: opts.rewrites,
44+
}
45+
}
46+
}
47+
48+
/// Lifecycle
49+
impl Options {
50+
#[cfg(feature = "blob-diff")]
51+
pub(crate) fn from_configuration(config: &crate::config::Cache) -> Result<Self, options::init::Error> {
52+
Ok(Options {
53+
location: Some(Location::Path),
54+
rewrites: config.diff_renames()?.unwrap_or_default().into(),
55+
})
56+
}
57+
}
58+
59+
/// Setters
60+
impl Options {
61+
/// Do not keep track of filepaths at all, which will leave all `location` fields empty.
62+
pub fn no_locations(&mut self) -> &mut Self {
63+
self.location = Some(Location::FileName);
64+
self
65+
}
66+
67+
/// Keep track of file-names, which makes `location` fields usable with the filename of the changed item.
68+
pub fn track_filename(&mut self) -> &mut Self {
69+
self.location = Some(Location::FileName);
70+
self
71+
}
72+
73+
/// Keep track of the entire path of a change, relative to the repository. (default).
74+
///
75+
/// This makes the `location` field fully usable.
76+
pub fn track_path(&mut self) -> &mut Self {
77+
self.location = Some(Location::Path);
78+
self
79+
}
80+
81+
/// Provide `None` to disable rewrite tracking entirely, or pass `Some(<configuration>)` to control to
82+
/// what extent rename and copy tracking is performed.
83+
///
84+
/// Note that by default, the git configuration determines rewrite tracking and git defaults are used
85+
/// if nothing is configured, which turns rename tracking with 50% similarity on, while not tracking copies at all.
86+
#[cfg(feature = "blob-diff")]
87+
pub fn track_rewrites(&mut self, renames: Option<gix_diff::Rewrites>) -> &mut Self {
88+
self.rewrites = renames;
89+
self
90+
}
91+
}
92+
93+
/// Builder
94+
impl Options {
95+
/// Provide `None` to disable rewrite tracking entirely, or pass `Some(<configuration>)` to control to
96+
/// what extent rename and copy tracking is performed.
97+
///
98+
/// Note that by default, the git configuration determines rewrite tracking and git defaults are used
99+
/// if nothing is configured, which turns rename tracking with 50% similarity on, while not tracking copies at all.
100+
#[cfg(feature = "blob-diff")]
101+
pub fn with_rewrites(mut self, renames: Option<gix_diff::Rewrites>) -> Self {
102+
self.rewrites = renames;
103+
self
104+
}
105+
}
106+
3107
///
4108
pub mod rename {
5109
/// Determine how to do rename tracking.

gix/src/object/tree/diff/for_each.rs

+2-4
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ impl<'old> Platform<'_, 'old> {
7474
}
7575
Some(cache) => cache,
7676
};
77+
let opts = self.options.into();
7778
Ok(gix_diff::tree_with_rewrites(
7879
TreeRefIter::from_bytes(&self.lhs.data),
7980
TreeRefIter::from_bytes(&other.data),
@@ -86,10 +87,7 @@ impl<'old> Platform<'_, 'old> {
8687
Action::Cancel => gix_diff::tree_with_rewrites::Action::Cancel,
8788
})
8889
},
89-
gix_diff::tree_with_rewrites::Options {
90-
location: self.location,
91-
rewrites: self.rewrites,
92-
},
90+
opts,
9391
)?)
9492
}
9593
}

gix/src/object/tree/diff/mod.rs

+8-36
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use gix_diff::tree;
2-
use gix_diff::tree::recorder::Location;
32

4-
use crate::{bstr::BStr, diff::Rewrites, Id, Tree};
3+
use crate::{bstr::BStr, Id, Tree};
54

65
/// Returned by the `for_each` function to control flow.
76
#[derive(Default, Clone, Copy, PartialOrd, PartialEq, Ord, Eq, Hash)]
@@ -86,7 +85,7 @@ pub enum Change<'a, 'old, 'new> {
8685
source_entry_mode: gix_object::tree::EntryMode,
8786
/// The object id of the entry before the rename.
8887
///
89-
/// Note that this is the same as `id` if we require the [similarity to be 100%](Rewrites::percentage), but may
88+
/// Note that this is the same as `id` if we require the [similarity to be 100%](gix_diff::Rewrites::percentage), but may
9089
/// be different otherwise.
9190
source_id: Id<'old>,
9291
/// Information about the diff we performed to detect similarity and match the `source_id` with the current state at `id`.
@@ -129,12 +128,11 @@ impl<'repo> Tree<'repo> {
129128
/// using [`Platform::track_rewrites()`].
130129
#[allow(clippy::result_large_err)]
131130
#[doc(alias = "diff_tree_to_tree", alias = "git2")]
132-
pub fn changes<'a>(&'a self) -> Result<Platform<'a, 'repo>, crate::diff::new_rewrites::Error> {
131+
pub fn changes<'a>(&'a self) -> Result<Platform<'a, 'repo>, crate::diff::options::init::Error> {
133132
Ok(Platform {
134133
state: Default::default(),
135134
lhs: self,
136-
location: Some(Location::Path),
137-
rewrites: self.repo.config.diff_renames()?.unwrap_or_default().into(),
135+
options: crate::diff::Options::from_configuration(&self.repo.config)?,
138136
})
139137
}
140138
}
@@ -144,39 +142,13 @@ impl<'repo> Tree<'repo> {
144142
pub struct Platform<'a, 'repo> {
145143
state: gix_diff::tree::State,
146144
lhs: &'a Tree<'repo>,
147-
location: Option<Location>,
148-
rewrites: Option<Rewrites>,
145+
options: crate::diff::Options,
149146
}
150147

151-
/// Configuration
152148
impl Platform<'_, '_> {
153-
/// Do not keep track of filepaths at all, which will leave all [`location`][Change::location] fields empty.
154-
pub fn no_locations(&mut self) -> &mut Self {
155-
self.location = Some(Location::FileName);
156-
self
157-
}
158-
159-
/// Keep track of file-names, which makes the [`location`][Change::location] field usable with the filename of the changed item.
160-
pub fn track_filename(&mut self) -> &mut Self {
161-
self.location = Some(Location::FileName);
162-
self
163-
}
164-
165-
/// Keep track of the entire path of a change, relative to the repository. (default).
166-
///
167-
/// This makes the [`location`][Change::location] field usable.
168-
pub fn track_path(&mut self) -> &mut Self {
169-
self.location = Some(Location::Path);
170-
self
171-
}
172-
173-
/// Provide `None` to disable rewrite tracking entirely, or pass `Some(<configuration>)` to control to
174-
/// what extent rename and copy tracking is performed.
175-
///
176-
/// Note that by default, the git configuration determines rewrite tracking and git defaults are used
177-
/// if nothing is configured, which turns rename tracking with 50% similarity on, while not tracking copies at all.
178-
pub fn track_rewrites(&mut self, renames: Option<Rewrites>) -> &mut Self {
179-
self.rewrites = renames;
149+
/// Adjust diff options with `change_opts`.
150+
pub fn options(&mut self, change_opts: impl FnOnce(&mut crate::diff::Options)) -> &mut Self {
151+
change_opts(&mut self.options);
180152
self
181153
}
182154
}

gix/src/repository/diff.rs

+42-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
use crate::repository::diff_resource_cache;
2-
use crate::Repository;
1+
use crate::repository::{diff_resource_cache, diff_tree_to_tree};
2+
use crate::{Repository, Tree};
3+
use gix_object::TreeRefIter;
34

45
/// Diff-utilities
56
impl Repository {
@@ -35,6 +36,45 @@ impl Repository {
3536
)?)
3637
}
3738

39+
/// Produce the changes that would need to be applied to `old_tree` to create `new_tree`.
40+
/// If `options` are unset, they will be filled in according to the git configuration of this repository, and with
41+
/// [full paths being tracked](crate::object::tree::diff::Platform::track_path()) as well, which typically means that
42+
/// rewrite tracking might be disabled if done so explicitly by the user.
43+
/// If `options` are set, the user can take full control over the settings.
44+
///
45+
/// Note that this method exists to evoke similarity to `git2`, and makes it easier to fully control diff settings.
46+
/// A more fluent version [may be used as well](Tree::changes()).
47+
pub fn diff_tree_to_tree<'a, 'old_repo: 'a, 'new_repo: 'a>(
48+
&self,
49+
old_tree: impl Into<Option<&'a Tree<'old_repo>>>,
50+
new_tree: impl Into<Option<&'a Tree<'new_repo>>>,
51+
options: impl Into<Option<crate::diff::Options>>,
52+
) -> Result<Vec<gix_diff::tree_with_rewrites::Change>, diff_tree_to_tree::Error> {
53+
let mut cache = self.diff_resource_cache(gix_diff::blob::pipeline::Mode::ToGit, Default::default())?;
54+
let opts = options
55+
.into()
56+
.map_or_else(|| crate::diff::Options::from_configuration(&self.config), Ok)?
57+
.into();
58+
59+
let empty_tree = self.empty_tree();
60+
let old_tree = old_tree.into().unwrap_or(&empty_tree);
61+
let new_tree = new_tree.into().unwrap_or(&empty_tree);
62+
let mut out = Vec::new();
63+
gix_diff::tree_with_rewrites(
64+
TreeRefIter::from_bytes(&old_tree.data),
65+
TreeRefIter::from_bytes(&new_tree.data),
66+
&mut cache,
67+
&mut Default::default(),
68+
&self.objects,
69+
|change| -> Result<_, std::convert::Infallible> {
70+
out.push(change.into_owned());
71+
Ok(gix_diff::tree_with_rewrites::Action::Continue)
72+
},
73+
opts,
74+
)?;
75+
Ok(out)
76+
}
77+
3878
/// Return a resource cache suitable for diffing blobs from trees directly, where no worktree checkout exists.
3979
///
4080
/// For more control, see [`diff_resource_cache()`](Self::diff_resource_cache).

gix/src/repository/mod.rs

+16
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,22 @@ mod submodule;
5858
mod thread_safe;
5959
mod worktree;
6060

61+
///
62+
#[cfg(feature = "blob-diff")]
63+
pub mod diff_tree_to_tree {
64+
/// The error returned by [Repository::diff_tree_to_tree()](crate::Repository::diff_tree_to_tree()).
65+
#[derive(Debug, thiserror::Error)]
66+
#[allow(missing_docs)]
67+
pub enum Error {
68+
#[error(transparent)]
69+
DiffOptions(#[from] crate::diff::options::init::Error),
70+
#[error(transparent)]
71+
CreateResourceCache(#[from] super::diff_resource_cache::Error),
72+
#[error(transparent)]
73+
TreeDiff(#[from] gix_diff::tree_with_rewrites::Error),
74+
}
75+
}
76+
6177
///
6278
#[cfg(feature = "blob-merge")]
6379
pub mod blob_merge_options {

0 commit comments

Comments
 (0)