Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: GitoxideLabs/gitoxide
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 28a8e93ec091511a348f0454e9d97e0403cdd3ab
Choose a base ref
..
head repository: GitoxideLabs/gitoxide
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: a8673224e8eca01b2d73088775efa68cdc5fdb1b
Choose a head ref
Showing with 132 additions and 9 deletions.
  1. +3 −0 Cargo.lock
  2. +3 −1 gix-diff/Cargo.toml
  3. +31 −3 gix-diff/src/index/function.rs
  4. +1 −0 gix-diff/tests/Cargo.toml
  5. +94 −5 gix-diff/tests/diff/index.rs
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion gix-diff/Cargo.toml
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ default = ["blob", "index"]
## Enable diffing of blobs using imara-diff.
blob = ["dep:imara-diff", "dep:gix-filter", "dep:gix-worktree", "dep:gix-path", "dep:gix-fs", "dep:gix-command", "dep:gix-tempfile", "dep:gix-trace", "dep:gix-traverse"]
## Enable diffing of two indices, which also allows for a generic rewrite tracking implementation.
index = ["dep:gix-index"]
index = ["dep:gix-index", "dep:gix-pathspec", "dep:gix-attributes"]
## Data structures implement `serde::Serialize` and `serde::Deserialize`.
serde = ["dep:serde", "gix-hash/serde", "gix-object/serde", "gix-index?/serde"]
## Make it possible to compile to the `wasm32-unknown-unknown` target.
@@ -28,6 +28,8 @@ doctest = false

[dependencies]
gix-index = { version = "^0.37.0", path = "../gix-index", optional = true }
gix-pathspec = { version = "^0.8.1", path = "../gix-pathspec", optional = true }
gix-attributes = { version = "^0.23.1", path = "../gix-attributes", optional = true }
gix-hash = { version = "^0.15.1", path = "../gix-hash" }
gix-object = { version = "^0.46.1", path = "../gix-object" }
gix-filter = { version = "^0.16.0", path = "../gix-filter", optional = true }
34 changes: 31 additions & 3 deletions gix-diff/src/index/function.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
use super::{Action, ChangeRef, Error, RewriteOptions};
use crate::rewrites;
use bstr::{BStr, BString, ByteSlice};
use gix_filter::attributes::glob::pattern::Case;
use std::borrow::Cow;
use std::cell::RefCell;
use std::cmp::Ordering;

/// Produce an entry-by-entry diff between `lhs` and `rhs`, sending changes to `cb(change) -> Action` for consumption,
/// which would turn `lhs` into `rhs` if applied.
/// Use `pathspec` to reduce the set of entries to look at, and `pathspec_attributes` may be used by pathspecs that perform
/// attribute lookups.
///
/// If `cb` indicated that the operation should be cancelled, no error is triggered as this isn't supposed to
/// occur through user-interaction - this diff is typically too fast.
///
/// Note that rewrites will be emitted at the end, so no ordering can be assumed.
/// Note that rewrites will be emitted at the end, so no ordering can be assumed. They will only be tracked if
/// `rewrite_options` is `Some`. Note that the set of entries participating in rename tracking is affected by `pathspec`.
///
/// Return the outcome of the rewrite tracker if it was enabled.
///
@@ -25,6 +30,8 @@ pub fn diff<'rhs, 'lhs: 'rhs, E, Find>(
rhs: &'rhs gix_index::State,
mut cb: impl FnMut(ChangeRef<'lhs, 'rhs>) -> Result<Action, E>,
rewrite_options: Option<RewriteOptions<'_, Find>>,
pathspec: &mut gix_pathspec::Search,
pathspec_attributes: &mut dyn FnMut(&BStr, Case, bool, &mut gix_attributes::search::Outcome) -> bool,
) -> Result<Option<rewrites::Outcome>, Error>
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
@@ -41,9 +48,30 @@ where
return Err(Error::LhsHasUnmerged);
}

let lhs_range = lhs
.prefixed_entries_range(pathspec.common_prefix())
.unwrap_or_else(|| 0..lhs.entries().len());
let rhs_range = rhs
.prefixed_entries_range(pathspec.common_prefix())
.unwrap_or_else(|| 0..rhs.entries().len());

let pattern_matches = RefCell::new(|relative_path, entry: &gix_index::Entry| {
pathspec
.pattern_matching_relative_path(relative_path, Some(entry.mode.is_submodule()), pathspec_attributes)
.map_or(false, |m| !m.is_excluded())
});

let (mut lhs_iter, mut rhs_iter) = (
lhs.entries().iter().enumerate().map(|(idx, e)| (idx, e.path(lhs), e)),
rhs.entries().iter().enumerate().map(|(idx, e)| (idx, e.path(rhs), e)),
lhs.entries()[lhs_range.clone()]
.iter()
.enumerate()
.map(|(idx, e)| (idx + lhs_range.start, e.path(lhs), e))
.filter(|(_, path, e)| pattern_matches.borrow_mut()(path, e)),
rhs.entries()[rhs_range.clone()]
.iter()
.enumerate()
.map(|(idx, e)| (idx + rhs_range.start, e.path(rhs), e))
.filter(|(_, path, e)| pattern_matches.borrow_mut()(path, e)),
);

let mut conflicting_paths = Vec::<BString>::new();
1 change: 1 addition & 0 deletions gix-diff/tests/Cargo.toml
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ path = "diff/main.rs"
[dev-dependencies]
gix-diff = { path = ".." }
gix-index = { version = "^0.37.0", path = "../../gix-index" }
gix-pathspec = { version = "^0.8.1", path = "../../gix-pathspec" }
gix-hash = { path = "../../gix-hash" }
gix-fs = { path = "../../gix-fs" }
gix-worktree = { path = "../../gix-worktree" }
99 changes: 94 additions & 5 deletions gix-diff/tests/diff/index.rs
Original file line number Diff line number Diff line change
@@ -44,12 +44,14 @@ fn empty_to_new_tree_without_rename_tracking() -> crate::Result {
"#);

{
let (lhs, rhs, _cache, _odb) = repo_with_indices(None, "c1 - initial")?;
let (lhs, rhs, _cache, _odb, mut pathspec) = repo_with_indices(None, "c1 - initial", None)?;
let err = gix_diff::index(
&lhs,
&rhs,
|_change| Err(std::io::Error::new(std::io::ErrorKind::Other, "custom error")),
None::<gix_diff::index::RewriteOptions<'_, gix_odb::Handle>>,
&mut pathspec,
&mut |_, _, _, _| true,
)
.unwrap_err();
assert_eq!(
@@ -1106,6 +1108,73 @@ fn realistic_renames_3_without_identity() -> crate::Result {
assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0);
assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0);

let (changes, _out) = collect_changes_opts_with_pathspec(
"r4-base",
"r4-dir-rename-non-identity",
Some(Rewrites {
copies: None,
percentage: None,
limit: 0,
track_empty: false,
}),
Some("src/plumbing/m*"),
)?;

// Pathspecs are applied in advance, which affects rename tracking.
insta::assert_debug_snapshot!(changes.into_iter().collect::<Vec<_>>(), @r#"
[
Deletion {
location: "src/plumbing/main.rs",
index: 2,
entry_mode: Mode(
FILE,
),
id: Sha1(d00491fd7e5bb6fa28c517a0bb32b8b506539d4d),
},
Deletion {
location: "src/plumbing/mod.rs",
index: 3,
entry_mode: Mode(
FILE,
),
id: Sha1(0cfbf08886fca9a91cb753ec8734c84fcbe52c9f),
},
]
"#);

let (changes, _out) = collect_changes_opts_with_pathspec(
"r4-base",
"r4-dir-rename-non-identity",
Some(Rewrites {
copies: None,
percentage: None,
limit: 0,
track_empty: false,
}),
Some("src/plumbing-renamed/m*"),
)?;
// One can also get the other side of the rename
insta::assert_debug_snapshot!(changes.into_iter().collect::<Vec<_>>(), @r#"
[
Addition {
location: "src/plumbing-renamed/main.rs",
index: 2,
entry_mode: Mode(
FILE,
),
id: Sha1(d00491fd7e5bb6fa28c517a0bb32b8b506539d4d),
},
Addition {
location: "src/plumbing-renamed/mod.rs",
index: 3,
entry_mode: Mode(
FILE,
),
id: Sha1(0cfbf08886fca9a91cb753ec8734c84fcbe52c9f),
},
]
"#);

Ok(())
}

@@ -1173,7 +1242,7 @@ fn unmerged_entries_and_intent_to_add() -> crate::Result {
]
"#);

let (index, _, _, _) = repo_with_indices(".git/index", ".git/index")?;
let (index, _, _, _, _) = repo_with_indices(".git/index", ".git/index", None)?;
assert_eq!(
index.entry_by_path("will-add".into()).map(|e| e.id),
Some(hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391")),
@@ -1195,11 +1264,13 @@ mod util {
pub fn repo_with_indices(
lhs: impl Into<Option<&'static str>>,
rhs: impl Into<Option<&'static str>>,
patterns: impl IntoIterator<Item = &'static str>,
) -> gix_testtools::Result<(
gix_index::State,
gix_index::State,
gix_diff::blob::Platform,
gix_odb::Handle,
gix_pathspec::Search,
)> {
let root = repo_workdir()?;
let odb = gix_odb::at(root.join(".git/objects"))?;
@@ -1218,7 +1289,14 @@ mod util {
Vec::new(),
),
);
Ok((lhs, rhs, cache, odb))
let pathspecs = gix_pathspec::Search::from_specs(
patterns
.into_iter()
.map(|p| gix_pathspec::Pattern::from_bytes(p.as_bytes(), Default::default()).expect("valid pattern")),
None,
&root,
)?;
Ok((lhs, rhs, cache, odb, pathspecs))
}

pub fn collect_changes_no_renames(
@@ -1233,7 +1311,16 @@ mod util {
rhs: impl Into<Option<&'static str>>,
options: Option<gix_diff::Rewrites>,
) -> gix_testtools::Result<(Vec<gix_diff::index::Change>, Option<rewrites::Outcome>)> {
let (from, to, mut cache, odb) = repo_with_indices(lhs, rhs)?;
collect_changes_opts_with_pathspec(lhs, rhs, options, None)
}

pub fn collect_changes_opts_with_pathspec(
lhs: impl Into<Option<&'static str>>,
rhs: impl Into<Option<&'static str>>,
options: Option<gix_diff::Rewrites>,
patterns: impl IntoIterator<Item = &'static str>,
) -> gix_testtools::Result<(Vec<gix_diff::index::Change>, Option<rewrites::Outcome>)> {
let (from, to, mut cache, odb, mut pathspecs) = repo_with_indices(lhs, rhs, patterns)?;
let mut out = Vec::new();
let rewrites_info = gix_diff::index(
&from,
@@ -1247,6 +1334,8 @@ mod util {
resource_cache: &mut cache,
find: &odb,
}),
&mut pathspecs,
&mut |_, _, _, _| false,
)?;
Ok((out, rewrites_info))
}
@@ -1275,4 +1364,4 @@ mod util {
}
}
use crate::hex_to_id;
use util::{collect_changes_no_renames, collect_changes_opts, repo_with_indices};
use util::{collect_changes_no_renames, collect_changes_opts, collect_changes_opts_with_pathspec, repo_with_indices};